Workaround para System.Text.Json

18 minuto(s) de leitura - March 21, 2020

01

Fala pessoal, tudo bem?!

Nesse artigo iremos descobrir como resolver um pequeno GAP que temos ao usar o System.Text.Json como nosso serializador.

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 😎). 01

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.

01

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);
}

01


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.

Twitter

Fico por aqui e um forte abraço! 😄
Me siga no twitter: @ralmsdeveloper
Dúvidas, quer bater um papo? Entre em contato comigo: ralms@ralms.net


Deixe um comentário