Condividi tramite


Personalizzare un contratto JSON

La libreria System.Text.Json costruisce un contratto JSON per ogni tipo .NET, che definisce il modo in cui il tipo deve essere serializzato e deserializzato. Il contratto è derivato dalla forma del tipo, che include caratteristiche quali le proprietà e i campi e se implementa l'interfaccia IEnumerable o IDictionary. I tipi vengono mappati ai contratti in fase di esecuzione usando la reflection o in fase di compilazione usando il generatore di origine.

A partire da .NET 7, è possibile personalizzare questi contratti JSON per fornire maggiore controllo sul modo in cui i tipi vengono convertiti in JSON e viceversa. L'elenco seguente mostra solo alcuni esempi dei tipi di personalizzazioni che è possibile eseguire per la serializzazione e la deserializzazione:

  • Serializzare campi e proprietà privati.
  • Supportare più nomi per una singola proprietà, ad esempio se una versione precedente della libreria ha usato un nome diverso.
  • Ignorare le proprietà con un nome, un tipo o un valore specifici.
  • Distinguere tra i valori null espliciti e la mancanza di un valore nel payload JSON.
  • Supportare attributi System.Runtime.Serialization, come DataContractAttribute. Per altre informazioni, vedere Attributi di System.Runtime.Serialization.
  • Generare un'eccezione se JSON include una proprietà che non fa parte del tipo di destinazione. Per altre informazioni, vedere Gestire i membri mancanti.

Come acconsentire esplicitamente

Esistono due modi per collegarsi alla personalizzazione. Entrambi implicano l'acquisizione di un resolver, il cui compito è quello di fornire un'istanza di JsonTypeInfo per ogni tipo che deve essere serializzato.

  • Chiamando il costruttore DefaultJsonTypeInfoResolver() per ottenere JsonSerializerOptions.TypeInfoResolver e aggiungendo le azioni personalizzate alla relativa proprietà Modifiers.

    Ad esempio:

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

    Se si aggiungono più modificatori, questi verranno chiamati in sequenza.

  • Scrivendo un resolver personalizzato che implementa IJsonTypeInfoResolver.

    • Se un tipo non viene gestito, IJsonTypeInfoResolver.GetTypeInfo deve restituire null per tale tipo.
    • È anche possibile combinare il resolver personalizzato con altri, ad esempio il resolver predefinito. I resolver verranno sottoposti a query in ordine fino a quando non viene restituito un valore JsonTypeInfo non Null per il tipo.

Aspetti configurabili

La proprietà JsonTypeInfo.Kind indica come il convertitore serializza un determinato tipo, ad esempio come oggetto o come matrice, e se le relative proprietà vengono serializzate. È possibile eseguire una query su questa proprietà per determinare quali aspetti del contratto JSON di un tipo è possibile configurare. Esistono quattro tipi diversi:

JsonTypeInfo.Kind Descrizione
JsonTypeInfoKind.Object Il convertitore serializzerà il tipo in un oggetto JSON e ne userà le proprietà. Questo tipo viene usato per la maggior parte dei tipi di classe e struct e consente la massima flessibilità.
JsonTypeInfoKind.Enumerable Il convertitore serializzerà il tipo in una matrice JSON. Questo tipo viene usato per tipi come List<T> e le matrici.
JsonTypeInfoKind.Dictionary Il convertitore serializzerà il tipo in un oggetto JSON. Questo tipo viene usato per tipi come Dictionary<K, V>.
JsonTypeInfoKind.None Il convertitore non specifica come serializzerà il tipo o quali proprietà JsonTypeInfo userà. Questo tipo viene usato per tipi come System.Object, int e string e per tutti i tipi che usano un convertitore personalizzato.

Modificatori

Un modificatore è Action<JsonTypeInfo> o un metodo con un parametro JsonTypeInfo che ottiene lo stato corrente del contratto come argomento e apporta modifiche al contratto. Ad esempio, è possibile eseguire l'iterazione delle proprietà prepopolate per il JsonTypeInfo specificato per trovare quello a cui si è interessati e quindi modificarne la proprietà JsonPropertyInfo.Get (per la serializzazione) o la proprietà JsonPropertyInfo.Set (per la deserializzazione). In alternativa, è possibile costruire una nuova proprietà usando JsonTypeInfo.CreateJsonPropertyInfo(Type, String) e aggiungerla alla raccolta JsonTypeInfo.Properties.

La tabella seguente illustra le modifiche che è possibile apportare e come ottenerle.

Modifica JsonTypeInfo.Kind applicabile Procedura Esempio
Personalizzare il valore di una proprietà JsonTypeInfoKind.Object Modificare il delegato JsonPropertyInfo.Get (per la serializzazione) o il delegato JsonPropertyInfo.Set (per la deserializzazione) per la proprietà. Incrementare il valore di una proprietà
Aggiungere o rimuovere proprietà JsonTypeInfoKind.Object Aggiungere o rimuovere elementi dall'elenco JsonTypeInfo.Properties. Serializzare campi privati
Serializzare in modo condizionale una proprietà JsonTypeInfoKind.Object Modificare il predicato JsonPropertyInfo.ShouldSerialize per la proprietà. Ignorare le proprietà con un tipo specifico
Personalizzare la gestione dei numeri per un tipo specifico JsonTypeInfoKind.None Modificare il valore JsonTypeInfo.NumberHandling per il tipo. Consentire a valori int di essere stringhe

Esempio: incrementare il valore di una proprietà

Si consideri l'esempio seguente in cui il modificatore incrementa il valore di una determinata proprietà per la deserializzazione modificandone il delegato JsonPropertyInfo.Set. Oltre a definire il modificatore, nell'esempio viene introdotto anche un nuovo attributo usato per individuare la proprietà il cui valore deve essere incrementato. Questo è un esempio di personalizzazione di una proprietà.

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

Si noti nell'output che il valore di RoundTrips viene incrementato ogni volta che l'istanza di Product viene deserializzata.

Esempio: serializzare campi privati

Per impostazione predefinita, System.Text.Json ignora i campi privati e le proprietà. In questo esempio viene aggiunto un nuovo attributo a livello di classe, JsonIncludePrivateFieldsAttribute, per modificare tale impostazione predefinita. Se il modificatore trova l'attributo in un tipo, aggiunge tutti i campi privati del tipo come nuove proprietà 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]
        }
    }
}

Suggerimento

Se i nomi dei campi privati iniziano con caratteri di sottolineatura, è consigliabile rimuovere i caratteri di sottolineatura dai nomi quando si aggiungono i campi come nuove proprietà JSON.

Esempio: ignorare le proprietà con un tipo specifico

È possibile che il modello includa proprietà con nomi o tipi specifici che non si vogliono esporre agli utenti. Ad esempio, si potrebbe avere una proprietà che archivia le credenziali o alcune informazioni che non ha senso includere nel payload.

Nell'esempio seguente viene illustrato come filtrare le proprietà con un tipo specifico, SecretHolder. A tale scopo viene usato un metodo di estensione IList<T> per rimuovere tutte le proprietà con il tipo specificato dall'elenco JsonTypeInfo.Properties. Le proprietà filtrate scompaiono completamente dal contratto, il che significa che System.Text.Json non le esamina durante la serializzazione o la deserializzazione.

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

Esempio: consentire a valori int di essere stringhe

È possibile che il codice JSON di input contenga virgolette che racchiudono uno dei tipi numerici, ma non altri. Se si avesse il controllo sulla classe, si potrebbe applicare JsonNumberHandlingAttribute per il tipo per risolvere il problema, ma non è questo il caso. Prima di .NET 7, sarebbe stato necessario scrivere un convertitore personalizzato per correggere questo comportamento, operazione che richiede la scrittura di una certa quantità di codice. Usando la personalizzazione del contratto, è possibile personalizzare il comportamento di gestione dei numeri per qualsiasi tipo.

Nell'esempio seguente viene modificato il comportamento per tutti i valori int. L'esempio può essere facilmente adattato per essere applicato a qualsiasi tipo o per una proprietà specifica di qualsiasi 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)
        }
    }
}

Senza il modificatore per consentire la lettura di valori int da una stringa, il programma terminerebbe con un'eccezione:

Eccezione non gestita. System.Text.Json.JsonException: impossibile convertire il valore JSON in System.Int32. Percorso: $.X | LineNumber: 0 | BytePositionInLine: 9.

Altri modi per personalizzare la serializzazione

Oltre a personalizzare un contratto, esistono altri modi per influenzare il comportamento di serializzazione e deserializzazione, tra cui:

  • Uso degli attributi derivati da JsonAttribute, ad esempio JsonIgnoreAttribute e JsonPropertyOrderAttribute.
  • Modifica di JsonSerializerOptions, ad esempio, per impostare un criterio di denominazione o serializzare i valori di enumerazione come stringhe anziché numeri.
  • Scrittura di un convertitore personalizzato che esegue il lavoro effettivo di scrittura del codice JSON e, durante la deserializzazione, costruisce un oggetto.

La personalizzazione del contratto è un miglioramento rispetto a queste personalizzazioni preesistenti, perché potrebbe non essere possibile accedere al tipo per aggiungere attributi e la scrittura di un convertitore personalizzato è complessa e influisce sulle prestazioni.

Vedi anche