Partilhar via


Personalizar um contrato JSON

A System.Text.Json biblioteca constrói um contrato JSON para cada tipo .NET, 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 IEnumerable interface ou IDictionary . Os tipos são mapeados para contratos em tempo de execução usando reflexão ou em tempo de compilação usando o gerador de código-fonte.

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

  • Serialize campos e propriedades particulares.
  • Ofereça suporte a vários nomes para uma única propriedade (por exemplo, se uma versão anterior da biblioteca usava um nome diferente).
  • Ignore propriedades com um nome, tipo ou valor específico.
  • Distinga entre valores explícitos null e a falta de um valor na carga útil JSON.
  • Atributos de suporte System.Runtime.Serialization , como DataContractAttribute. Para obter mais informações, consulte Atributos System.Runtime.Serialization.
  • Lance uma exceção se o JSON incluir uma propriedade que não faz parte do tipo de destino. Para obter mais informações, consulte Manipular membros ausentes.

Como aderir

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

  • Chamando o DefaultJsonTypeInfoResolver() construtor para obter o JsonSerializerOptions.TypeInfoResolver e adicionando suas ações personalizadas à sua Modifiers propriedade.

    Por exemplo:

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

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

  • Escrevendo um resolvedor personalizado que implementa o IJsonTypeInfoResolver.

    • Se um tipo não for manipulado, IJsonTypeInfoResolver.GetTypeInfo deve 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.

Aspetos configuráveis

A JsonTypeInfo.Kind propriedade indica como o conversor serializa um 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 aspetos do contrato JSON de um tipo você pode configurar. Existem quatro tipos diferentes:

JsonTypeInfo.Kind Description
JsonTypeInfoKind.Object O conversor serializará o tipo em um objeto JSON e usará suas propriedades. Este 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. Este tipo é usado para tipos como List<T> e matriz.
JsonTypeInfoKind.Dictionary O conversor serializará o tipo em um objeto JSON. Este tipo é usado para tipos como Dictionary<K, V>.
JsonTypeInfoKind.None O conversor não especifica como serializará o tipo ou quais JsonTypeInfo propriedades usará. Esse tipo é usado para tipos como System.Object, inte string, e para todos os tipos que usam um conversor personalizado.

Modificadores

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

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

Modificação Aplicável JsonTypeInfo.Kind Como alcançá-lo Exemplo
Personalizar o valor de uma propriedade JsonTypeInfoKind.Object Modifique o delegado (para serialização) ou JsonPropertyInfo.Set delegado JsonPropertyInfo.Get (para desserialização) para a propriedade. Incrementar o valor de uma propriedade
Adicionar ou remover propriedades JsonTypeInfoKind.Object Adicione ou remova itens da JsonTypeInfo.Properties lista. Serializar campos privados
Serializar condicionalmente uma propriedade JsonTypeInfoKind.Object Modifique o predicado JsonPropertyInfo.ShouldSerialize da propriedade. Ignorar propriedades com um tipo específico
Personalizar a manipulação de números para um tipo específico JsonTypeInfoKind.None Modifique o JsonTypeInfo.NumberHandling valor para o tipo. Permitir que os 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 uma determinada propriedade na desserialização modificando seu JsonPropertyInfo.Set delegado. Além de definir o modificador, o exemplo também introduz um novo atributo que ele usa para localizar a propriedade cujo valor deve ser incrementado. Este é um exemplo de personalização de 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 é incrementado RoundTrips cada vez que a Product instância é desserializada.

Exemplo: Serializar campos privados

Por padrão, System.Text.Json ignora campos e propriedades particulares. Este 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 ao 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]
        }
    }
}

Gorjeta

Se os nomes de campos privados começarem com sublinhados, considere remover os sublinhados dos nomes ao 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 ter na carga.

O exemplo a seguir mostra como filtrar propriedades com um tipo específico, SecretHolder. Ele faz isso usando um IList<T> método de extensão para remover quaisquer propriedades que tenham o tipo especificado da JsonTypeInfo.Properties lista. As propriedades filtradas desaparecem completamente do contrato, o que significa System.Text.Json que não as examina 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 conter aspas em torno de um dos tipos numéricos, mas não em outros. Se você tivesse controle sobre a classe, poderia colocar JsonNumberHandlingAttribute o tipo para corrigir isso, mas não o faz. Antes do .NET 7, você precisaria escrever um conversor personalizado para corrigir esse comportamento, o que requer escrever um pouco de código. Usando a personalização de contrato, você pode personalizar o comportamento de manipulação de números para qualquer tipo.

O exemplo a seguir altera o comportamento de todos os int valores. O exemplo pode ser facilmente ajustado para ser aplicado a qualquer tipo ou para 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 int de valores de uma cadeia de caracteres, o programa teria terminado com uma exceção:

Exceção não tratada. System.Text.Json.JsonException: O valor JSON não pôde ser convertido para System.Int32. Caminho: $. X • Número de linha: 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:

  • Usando atributos derivados de JsonAttribute, por exemplo, JsonIgnoreAttribute e JsonPropertyOrderAttribute.
  • Modificando 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.
  • Escrevendo um conversor personalizado que faz o trabalho real de escrever o JSON e, durante a desserialização, construir um objeto.

A personalização de contrato é uma melhoria em relação a essas personalizações pré-existentes, porque você pode não ter acesso ao tipo para adicionar atributos, e escrever um conversor personalizado é complexo e prejudica o desempenho.

Consulte também