Compartilhar via


Personalizar um contrato JSON

A biblioteca System.Text.Json constrói um contrato JSON para cada tipo .NET, o que define como o tipo deve ser serializado e desserializado. O contrato é derivado da forma do tipo, que inclui características como suas propriedades e campos e se ele implementa a interface IEnumerable ou IDictionary. Os tipos são mapeados para contratos no tempo de execução com o uso de reflexão ou no tempo de compilação com o uso do gerador de origem.

A partir do .NET 7, você pode personalizar esses contratos JSON para oferecer mais controle sobre como os tipos são convertidos em JSON e vice-versa. A lista abaixo mostra apenas alguns exemplos dos tipos de personalização que você pode fazer para serialização e desserialização:

  • Serializar campos e propriedades privados.
  • Dar suporte a vários nomes para uma única propriedade (por exemplo, se uma versão anterior da biblioteca usou um nome diferente).
  • Ignorar propriedades com um nome, tipo ou valor específico.
  • Distinguir entre valores explícitos null e a falta de um valor no conteúdo JSON.
  • Atributos de suporte System.Runtime.Serialization, como DataContractAttribute. Para obter mais informações, confira atributos System.Runtime.Serialization.
  • Gere uma exceção se o JSON incluir uma propriedade que não faça parte do tipo de destino. Para obter mais informações, confira Manipular membros ausentes.

Como aceitar

Há duas maneiras de se conectar à personalização. Ambas envolvem a obtenção de um resolvedor, cujo trabalho é fornecer uma instância JsonTypeInfo para cada tipo que precisa ser serializado.

  • Chamada do construtor DefaultJsonTypeInfoResolver() para obtenção do JsonSerializerOptions.TypeInfoResolver e adição de suasações personalizadas à propriedade Modifiers.

    Por exemplo:

    JsonSerializerOptions options = new()
    {
        TypeInfoResolver = new DefaultJsonTypeInfoResolver
        {
            Modifiers =
            {
                MyCustomModifier1,
                MyCustomModifier2
            }
        }
    };
    

    se você adicionar vários modificadores, eles serão chamados sequencialmente.

  • Programação de um resolvedor personalizado que implementa IJsonTypeInfoResolver.

    • Se um tipo não for tratado, IJsonTypeInfoResolver.GetTypeInfo deverá retornar null para esse tipo.
    • Você também pode combinar seu resolvedor personalizado com outros, por exemplo, o resolvedor padrão. Os resolvedores serão consultados em ordem até que um valor não nulo JsonTypeInfo seja retornado para o tipo.

Aspectos configuráveis

A propriedade JsonTypeInfo.Kind indica como o conversor serializa determinado tipo (por exemplo, como um objeto ou como uma matriz) e se suas propriedades são serializadas. Você pode consultar essa propriedade para determinar quais aspectos do contrato JSON de um tipo você pode configurar. Há quatro tipos diferentes:

JsonTypeInfo.Kind Descrição
JsonTypeInfoKind.Object O conversor serializará o tipo em um objeto JSON e usará suas propriedades. Esse tipo é usado para a maioria dos tipos de classe e struct e permite a maior flexibilidade.
JsonTypeInfoKind.Enumerable O conversor serializará o tipo em uma matriz JSON. Esse tipo é usado para tipos como List<T> e matriz.
JsonTypeInfoKind.Dictionary O conversor serializará o tipo em um objeto JSON. Esse tipo é usado para tipos como Dictionary<K, V>.
JsonTypeInfoKind.None O conversor não especifica como serializará o tipo ou quais propriedades JsonTypeInfo ele usará. Esse tipo é usado para tipos como System.Object, int e string e para todos os tipos que usam um conversor personalizado.

Modificadores

Um modificador é uma classe Action<JsonTypeInfo> ou um método com um parâmetro JsonTypeInfo que obtém o estado atual do contrato como argumento e faz modificações no contrato. Por exemplo, você pode iterar por meio das propriedades pré-preenchidas na JsonTypeInfo especificada encontrar a que interessa e modificar sua propriedade JsonPropertyInfo.Get (para serialização) ou JsonPropertyInfo.Set propriedade (para desserialização). Ou pode construir uma nova propriedade usando JsonTypeInfo.CreateJsonPropertyInfo(Type, String) e adicioná-la à coleção JsonTypeInfo.Properties.

A tabela a seguir mostra as modificações que você pode fazer e como alcançá-las.

Modification JsonTypeInfo.Kind aplicável Como alcançá-las Exemplo
Personalizar o valor de uma propriedade JsonTypeInfoKind.Object Modifique o delegado JsonPropertyInfo.Get (para serialização) ou o delegado JsonPropertyInfo.Set (para desserialização) da propriedade. Incrementar um valor da propriedade
Adicionar ou remover propriedades JsonTypeInfoKind.Object Adicionar e remover itens da lista JsonTypeInfo.Properties. Serializar campos privados
Serializar condicionalmente uma propriedade JsonTypeInfoKind.Object Modifique o predicado JsonPropertyInfo.ShouldSerialize para a propriedade. Ignorar propriedades com um tipo específico
Personalizar o tratamento de números para um tipo específico JsonTypeInfoKind.None Modifique o valor JsonTypeInfo.NumberHandling para o tipo. Permitir que valores int sejam cadeias de caracteres

Exemplo: incrementar o valor de uma propriedade

Considere o exemplo a seguir em que o modificador incrementa o valor de determinada propriedade na desserialização modificando seu delegado JsonPropertyInfo.Set. Além de definir o modificador, o exemplo também apresenta um novo atributo que ele usa para localizar a propriedade cujo valor deve ser incrementado. Esse é um exemplo de como personalizar uma propriedade.

using System.Text.Json;
using System.Text.Json.Serialization.Metadata;

namespace Serialization
{
    // Custom attribute to annotate the property
    // we want to be incremented.
    [AttributeUsage(AttributeTargets.Property)]
    class SerializationCountAttribute : Attribute
    {
    }

    // Example type to serialize and deserialize.
    class Product
    {
        public string Name { get; set; } = "";
        [SerializationCount]
        public int RoundTrips { get; set; }
    }

    public class SerializationCountExample
    {
        // Custom modifier that increments the value
        // of a specific property on deserialization.
        static void IncrementCounterModifier(JsonTypeInfo typeInfo)
        {
            foreach (JsonPropertyInfo propertyInfo in typeInfo.Properties)
            {
                if (propertyInfo.PropertyType != typeof(int))
                    continue;

                object[] serializationCountAttributes = propertyInfo.AttributeProvider?.GetCustomAttributes(typeof(SerializationCountAttribute), true) ?? Array.Empty<object>();
                SerializationCountAttribute? attribute = serializationCountAttributes.Length == 1 ? (SerializationCountAttribute)serializationCountAttributes[0] : null;

                if (attribute != null)
                {
                    Action<object, object?>? setProperty = propertyInfo.Set;
                    if (setProperty is not null)
                    {
                        propertyInfo.Set = (obj, value) =>
                        {
                            if (value != null)
                            {
                                // Increment the value by 1.
                                value = (int)value + 1;
                            }

                            setProperty (obj, value);
                        };
                    }
                }
            }
        }

        public static void RunIt()
        {
            var product = new Product
            {
                Name = "Aquafresh"
            };

            JsonSerializerOptions options = new()
            {
                TypeInfoResolver = new DefaultJsonTypeInfoResolver
                {
                    Modifiers = { IncrementCounterModifier }
                }
            };

            // First serialization and deserialization.
            string serialized = JsonSerializer.Serialize(product, options);
            Console.WriteLine(serialized);
            // {"Name":"Aquafresh","RoundTrips":0}

            Product deserialized = JsonSerializer.Deserialize<Product>(serialized, options)!;
            Console.WriteLine($"{deserialized.RoundTrips}");
            // 1

            // Second serialization and deserialization.
            serialized = JsonSerializer.Serialize(deserialized, options);
            Console.WriteLine(serialized);
            // { "Name":"Aquafresh","RoundTrips":1}

            deserialized = JsonSerializer.Deserialize<Product>(serialized, options)!;
            Console.WriteLine($"{deserialized.RoundTrips}");
            // 2
        }
    }
}

Observe na saída que o valor de RoundTrips é incrementado sempre que a instância Product é desserializada.

Exemplo: serializar campos privados

Por padrão, System.Text.Json ignora campos e propriedades privados. Esse exemplo adiciona um novo atributo de toda a classe, JsonIncludePrivateFieldsAttribute, para alterar esse padrão. Se o modificador encontrar o atributo em um tipo, ele adicionará todos os campos privados no tipo como novas propriedades a JsonTypeInfo.

using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

namespace Serialization
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
    public class JsonIncludePrivateFieldsAttribute : Attribute { }

    [JsonIncludePrivateFields]
    public class Human
    {
        private string _name;
        private int _age;

        public Human()
        {
            // This constructor should be used only by deserializers.
            _name = null!;
            _age = 0;
        }

        public static Human Create(string name, int age)
        {
            Human h = new()
            {
                _name = name,
                _age = age
            };

            return h;
        }

        [JsonIgnore]
        public string Name
        {
            get => _name;
            set => throw new NotSupportedException();
        }

        [JsonIgnore]
        public int Age
        {
            get => _age;
            set => throw new NotSupportedException();
        }
    }

    public class PrivateFieldsExample
    {
        static void AddPrivateFieldsModifier(JsonTypeInfo jsonTypeInfo)
        {
            if (jsonTypeInfo.Kind != JsonTypeInfoKind.Object)
                return;

            if (!jsonTypeInfo.Type.IsDefined(typeof(JsonIncludePrivateFieldsAttribute), inherit: false))
                return;

            foreach (FieldInfo field in jsonTypeInfo.Type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic))
            {
                JsonPropertyInfo jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(field.FieldType, field.Name);
                jsonPropertyInfo.Get = field.GetValue;
                jsonPropertyInfo.Set = field.SetValue;

                jsonTypeInfo.Properties.Add(jsonPropertyInfo);
            }
        }

        public static void RunIt()
        {
            var options = new JsonSerializerOptions
            {
                TypeInfoResolver = new DefaultJsonTypeInfoResolver
                {
                    Modifiers = { AddPrivateFieldsModifier }
                }
            };

            var human = Human.Create("Julius", 37);
            string json = JsonSerializer.Serialize(human, options);
            Console.WriteLine(json);
            // {"_name":"Julius","_age":37}

            Human deserializedHuman = JsonSerializer.Deserialize<Human>(json, options)!;
            Console.WriteLine($"[Name={deserializedHuman.Name}; Age={deserializedHuman.Age}]");
            // [Name=Julius; Age=37]
        }
    }
}

Dica

Se os nomes de campo privados começarem com sublinhados, considere remover os sublinhados dos nomes quando você adicionar os campos como novas propriedades JSON.

Exemplo: ignorar propriedades com um tipo específico

Talvez seu modelo tenha propriedades com nomes ou tipos específicos que você não deseja expor aos usuários. Por exemplo, você pode ter uma propriedade que armazena credenciais ou algumas informações que são inúteis de se ter no conteúdo.

O exemplo a seguir mostra como filtrar propriedades com um tipo específico, SecretHolder. Ele faz isso usando um método de extensão IList<T> para remover propriedades que tenham o tipo especificado da lista JsonTypeInfo.Properties. As propriedades filtradas desaparecem completamente do contrato, o que significa que System.Text.Json não as analisa durante a serialização ou desserialização.

using System.Text.Json;
using System.Text.Json.Serialization.Metadata;

namespace Serialization
{
    class ExampleClass
    {
        public string Name { get; set; } = "";
        public SecretHolder? Secret { get; set; }
    }

    class SecretHolder
    {
        public string Value { get; set; } = "";
    }

    class IgnorePropertiesWithType
    {
        private readonly Type[] _ignoredTypes;

        public IgnorePropertiesWithType(params Type[] ignoredTypes)
            => _ignoredTypes = ignoredTypes;

        public void ModifyTypeInfo(JsonTypeInfo ti)
        {
            if (ti.Kind != JsonTypeInfoKind.Object)
                return;

            ti.Properties.RemoveAll(prop => _ignoredTypes.Contains(prop.PropertyType));
        }
    }

    public class IgnoreTypeExample
    {
        public static void RunIt()
        {
            var modifier = new IgnorePropertiesWithType(typeof(SecretHolder));

            JsonSerializerOptions options = new()
            {
                TypeInfoResolver = new DefaultJsonTypeInfoResolver
                {
                    Modifiers = { modifier.ModifyTypeInfo }
                }
            };

            ExampleClass obj = new()
            {
                Name = "Password",
                Secret = new SecretHolder { Value = "MySecret" }
            };

            string output = JsonSerializer.Serialize(obj, options);
            Console.WriteLine(output);
            // {"Name":"Password"}
        }
    }

    public static class ListHelpers
    {
        // IList<T> implementation of List<T>.RemoveAll method.
        public static void RemoveAll<T>(this IList<T> list, Predicate<T> predicate)
        {
            for (int i = 0; i < list.Count; i++)
            {
                if (predicate(list[i]))
                {
                    list.RemoveAt(i--);
                }
            }
        }
    }
}

Exemplo: permitir que os valores int sejam cadeias de caracteres

Talvez seu JSON de entrada possa ter aspas em torno de um dos tipos numéricos, mas não em outros. Se você tivesse controle sobre a classe, poderia colocar JsonNumberHandlingAttribute no tipo para corrigir isso, mas não tem. Antes do .NET 7, você precisaria escrever um conversor personalizado para corrigir esse comportamento, o que requer uma boa quantidade de programação. Usando a personalização do contrato, você pode personalizar o comportamento de tratamento de números para qualquer tipo.

O exemplo a seguir altera o comportamento de todos os valores int. O exemplo pode ser facilmente ajustado para se aplicar a qualquer tipo ou a uma propriedade específica de qualquer tipo.

using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

namespace Serialization
{
    public class Point
    {
        public int X { get; set; }
        public int Y { get; set; }
    }

    public class AllowIntsAsStringsExample
    {
        static void SetNumberHandlingModifier(JsonTypeInfo jsonTypeInfo)
        {
            if (jsonTypeInfo.Type == typeof(int))
            {
                jsonTypeInfo.NumberHandling = JsonNumberHandling.AllowReadingFromString;
            }
        }

        public static void RunIt()
        {
            JsonSerializerOptions options = new()
            {
                TypeInfoResolver = new DefaultJsonTypeInfoResolver
                {
                    Modifiers = { SetNumberHandlingModifier }
                }
            };

            // Triple-quote syntax is a C# 11 feature.
            Point point = JsonSerializer.Deserialize<Point>("""{"X":"12","Y":"3"}""", options)!;
            Console.WriteLine($"({point.X},{point.Y})");
            // (12,3)
        }
    }
}

Sem o modificador para permitir a leitura de valores int de uma cadeia de caracteres, o programa teria terminado com uma exceção:

Exceção sem tratamento. System.Text.Json.JsonException: o valor JSON não pôde ser convertido em System.Int32. Path: $.X | LineNumber: 0 | BytePositionInLine: 9.

Outras maneiras de personalizar a serialização

Além de personalizar um contrato, há outras maneiras de influenciar o comportamento de serialização e desserialização, incluindo o seguinte:

  • Uso de atributos derivados de JsonAttribute, por exemplo, JsonIgnoreAttribute e JsonPropertyOrderAttribute.
  • Modificação de JsonSerializerOptions, por exemplo, para definir uma política de nomenclatura ou serializar valores de enumeração como cadeias de caracteres em vez de números.
  • Ao escrever um conversor personalizado que faz o trabalho real de escrever o JSON e, durante a desserialização, construir um objeto.

A personalização do contrato é uma melhoria em relação a essas personalizações preexistentes, pois você pode não ter acesso ao tipo para adicionar atributos, e a criação de um conversor personalizado é complexa e prejudica o desempenho.

Confira também