Workaround para System.Text.Json
FYI: Isso não é um Deep-Dive em System.Text.Json.
Introdução
Acredito que você já saiba que System.Text.Json é uma nova opção para serializar objetos, escrita pela Microsoft e pelo próprio criador do Newtonsoft.Json, seu objetivo principal é performance e alocar menos dados na memória.
Quer saber sobre? acessa esse link, vamos focar em um problema que talvez você já tenha enfrentado.
GAP
Como nem tudo é mil maravilhas, ontem(20/03/2020) juntamente com meus amigos de trabalho estavamos tentando deserializar um JSON para uma classe que tinha construtores parametrizados e as propriedades eram readonly (Immutable), como eu fui ingênuo 🤖, então fui analisar melhor o que estava acontecendo, e o que descobri(ou não me lembrava) não foi nada agradável, simplesmente não temos suporte, e o backlog de pendências é enorme! Veja aqui
Cenário
Vamos montar um cenário para ver como podemos resolver esse GAP, mas já vou te dizendo que precisa escrever alguns BITS 👨💻.
Classe
Vamos ter como base a seguinte classe concreta, apenas com 2 (duas) propriedades para facilitar nosso exemplo.
public class Pessoa
{
public string Nome { get; }
public DateTime DataNascimento { get;}
public Pessoa(string nome, DateTime dataNascimento)
{
Nome = nome;
DataNascimento = dataNascimento;
}
}
Serializar
Vamos tentar serializar um objeto.
Showww, como você pode ver na imagem abaixo a serialização funcionou perfeitamente (como esperado 😎).
Agora vamos tentar deserializar
Observe na imagem abaixo que ao tentar fazer a deserialização é lançada uma exception, informando que não existe suporte para construtores parametrizados.
Tem solução?
Como diz o velho ditado para todo problema existe uma solução e ela veio olhando para esse exemplo aqui, que basicamente é fazer uma implementação de JsonConverter e adicionar ao pipeline de customização. Vamos para um exemplo prático.
JsonConverter Customizado
Esse é o código que escrevi para resolver o problema aborado aqui, observe que estou herdando de JsonConverter alguns comportamentos e sobrescrevendo os mesmo.
namespace System.Text.Json.Serialization
{
public abstract class JsonConverter<T> : JsonConverter
{
protected internal JsonConverter();
public override bool CanConvert(Type typeToConvert);
public abstract T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options);
public abstract void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options);
}
}
Se você querer ver mais informações sobre a API clique aqui, mas por hora vamos ver nosso código como ficou.
public class MyJsonConverter : JsonConverter<object>
{
public override bool CanConvert(Type typeToConvert)
=> typeToConvert
.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
.Length == 1;
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var propertiesInfo = new Dictionary<PropertyInfo, object>();
var properties = typeToConvert.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
var mapping = properties.ToDictionary(p => p.Name, p => p);
reader.Read();
for (; ; )
{
if (reader.TokenType != JsonTokenType.PropertyName && reader.TokenType != JsonTokenType.String)
{
break;
}
var propertyName = reader.GetString();
if (!mapping.TryGetValue(propertyName, out var property))
{
reader.Read();
}
else
{
var value = JsonSerializer.Deserialize(ref reader, property.PropertyType, options);
reader.Read();
propertiesInfo[property] = value;
}
}
var constructorInfo = typeToConvert.GetConstructors(BindingFlags.Public | BindingFlags.Instance)[0];
var parameters = constructorInfo.GetParameters();
var parameterValues = new object[parameters.Length];
for (var index = 0; index < parameters.Length; index++)
{
var parameterInfo = parameters[index];
var value = propertiesInfo.First(prop => prop.Key.Name.Equals(parameterInfo.Name, StringComparison.InvariantCultureIgnoreCase)).Value;
parameterValues[index] = value;
}
var @object = constructorInfo.Invoke(parameterValues);
return @object;
}
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
=> throw new NotImplementedException();
}
static void Main(string[] args)
{
var pessoa = new Pessoa("Rafael", DateTime.Now);
var json = JsonSerializer.Serialize(pessoa);
var options = new JsonSerializerOptions();
options.Converters.Add(new MyJsonConverter());
var objectPessoa = JsonSerializer.Deserialize<Pessoa>(json, options);
Console.WriteLine(json);
}
Código completo
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace SystemTextJsonWorkAround
{
class Program
{
static void Main(string[] args)
{
var pessoa = new Pessoa("Rafael", DateTime.Now);
// Serializar
var json = JsonSerializer.Serialize(pessoa);
// Deserializar
var options = new JsonSerializerOptions();
options.Converters.Add(new MyJsonConverter());
var objectPessoa = JsonSerializer.Deserialize<Pessoa>(json, options);
Console.WriteLine(json);
}
}
public class Pessoa
{
public string Nome { get; }
public DateTime DataNascimento { get; }
public Pessoa(string nome, DateTime dataNascimento)
{
Nome = nome;
DataNascimento = dataNascimento;
}
}
public class MyJsonConverter : JsonConverter<object>
{
public override bool CanConvert(Type typeToConvert)
=> typeToConvert
.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
.Length == 1;
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var propertiesInfo = new Dictionary<PropertyInfo, object>();
var properties = typeToConvert
.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
var mapping = properties.ToDictionary(p => p.Name, p => p);
reader.Read();
for (; ; )
{
if (reader.TokenType != JsonTokenType.PropertyName && reader.TokenType != JsonTokenType.String)
{
break;
}
var propertyName = reader.GetString();
if (!mapping.TryGetValue(propertyName, out var property))
{
reader.Read();
}
else
{
var value = JsonSerializer.Deserialize(ref reader, property.PropertyType, options);
reader.Read();
propertiesInfo[property] = value;
}
}
var constructorInfo = typeToConvert.GetConstructors(BindingFlags.Public | BindingFlags.Instance)[0];
var parameters = constructorInfo.GetParameters();
var parameterValues = new object[parameters.Length];
for (var index = 0; index < parameters.Length; index++)
{
var parameterInfo = parameters[index];
var value = propertiesInfo.First(prop => prop.Key.Name.Equals(parameterInfo.Name, StringComparison.InvariantCultureIgnoreCase)).Value;
parameterValues[index] = value;
}
var @object = constructorInfo.Invoke(parameterValues);
return @object;
}
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
=> throw new NotImplementedException();
}
}
Approach
Esse seria a melhor abordagem? Para suprir esse GAP talvez sim, mas o código obviamente precisaria de melhorias para cobrir todos cenários possíveis e colocar em produção, aqui eu procurei apenas mostrar como é possível adicionar serializadores customizados.
News
A novidade é que iremos ter esse suporte na versão .NET 5 que inclusive já temos a Preview veja aqui.
Me siga no twitter: @ralmsdeveloper
Dúvidas, quer bater um papo? Entre em contato comigo: [email protected]
Deixe um comentário