Sdílet prostřednictvím


Přizpůsobení kontraktu JSON

Knihovna System.Text.Json vytvoří kontrakt JSON pro každý typ .NET, který definuje, jak má být typ serializován a deserializován. Kontrakt je odvozen od tvaru typu, který zahrnuje vlastnosti, jako jsou jeho vlastnosti a pole a zda implementuje IEnumerable rozhraní nebo IDictionary rozhraní. Typy se mapují na kontrakty za běhu pomocí reflexe nebo v době kompilace pomocí generátoru zdroje.

Od verze .NET 7 můžete tyto kontrakty JSON přizpůsobit, abyste měli větší kontrolu nad tím, jak se typy převedou na JSON a naopak. Následující seznam ukazuje jen některé příklady typů přizpůsobení, které můžete provést pro serializaci a deserializaci:

  • Serializace privátních polí a vlastností
  • Podpora více názvů pro jednu vlastnost (například pokud předchozí verze knihovny použila jiný název).
  • Ignorujte vlastnosti s určitým názvem, typem nebo hodnotou.
  • Rozlišovat mezi explicitními null hodnotami a chybějící hodnotou v datové části JSON
  • Atributy podpory System.Runtime.Serialization , například DataContractAttribute. Další informace naleznete v tématu System.Runtime.Serialization atributy.
  • Vyvolá výjimku, pokud JSON obsahuje vlastnost, která není součástí cílového typu. Další informace naleznete v tématu Zpracování chybějících členů.

Jak se přihlásit

Existují dva způsoby, jak se připojit k přizpůsobení. Oba zahrnují získání překladače, jehož úlohou je poskytnout JsonTypeInfo instanci pro každý typ, který musí být serializován.

  • Voláním konstruktoru DefaultJsonTypeInfoResolver() JsonSerializerOptions.TypeInfoResolver získat a přidat vlastní akce do jeho Modifiers vlastnosti.

    Příklad:

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

    Pokud přidáte více modifikátorů, budou se volat postupně.

  • Napsáním vlastního překladače, který implementuje IJsonTypeInfoResolver.

    • Pokud se typ nezpracuje, IJsonTypeInfoResolver.GetTypeInfo měl by se null vrátit pro tento typ.
    • Můžete také kombinovat vlastní překladač s ostatními, například s výchozím překladačem. Překladače budou dotazovány v pořadí, dokud se pro typ nevrátí hodnota, která není null JsonTypeInfo .

Konfigurovatelné aspekty

Vlastnost JsonTypeInfo.Kind určuje, jak převaděč serializuje daný typ , například jako objekt nebo jako pole a zda jsou jeho vlastnosti serializovány. Pomocí dotazu na tuto vlastnost můžete určit, které aspekty kontraktu JSON typu můžete nakonfigurovat. Existují čtyři různé druhy:

JsonTypeInfo.Kind Popis
JsonTypeInfoKind.Object Převaděč serializuje typ do objektu JSON a použije jeho vlastnosti. Tento druh se používá pro většinu typů tříd a struktur a umožňuje největší flexibilitu.
JsonTypeInfoKind.Enumerable Převaděč serializuje typ do pole JSON. Tento druh se používá pro typy jako List<T> a pole.
JsonTypeInfoKind.Dictionary Převaděč serializuje typ do objektu JSON. Tento druh se používá pro typy, jako je Dictionary<K, V>.
JsonTypeInfoKind.None Převaděč neurčuje, jak bude serializovat typ nebo jaké JsonTypeInfo vlastnosti bude používat. Tento druh se používá pro typy, jako System.Objectje , inta string, a pro všechny typy, které používají vlastní převaděč.

Modifikátory

Modifikátor je Action<JsonTypeInfo> nebo metoda s parametrem JsonTypeInfo , který získá aktuální stav kontraktu jako argument a provede úpravy kontraktu. Můžete například iterovat předem vyplněnými vlastnostmi v zadaném zadaném JsonTypeInfo objektu, který vás zajímá, a pak upravit jeho JsonPropertyInfo.Get vlastnost (pro serializaci) nebo JsonPropertyInfo.Set vlastnost (pro deserializaci). Nebo můžete vytvořit novou vlastnost pomocí JsonTypeInfo.CreateJsonPropertyInfo(Type, String) a přidat ji do JsonTypeInfo.Properties kolekce.

Následující tabulka ukazuje úpravy, které můžete provést a jak je dosáhnout.

Modifikace Aplikovatelný JsonTypeInfo.Kind Jak toho dosáhnout Příklad
Přizpůsobení hodnoty vlastnosti JsonTypeInfoKind.Object JsonPropertyInfo.Get Upravte delegáta (pro serializaci) nebo JsonPropertyInfo.Set delegáta (pro deserializaci) pro vlastnost. Zvýšení hodnoty vlastnosti
Přidání nebo odebrání vlastností JsonTypeInfoKind.Object Přidejte nebo odeberte položky ze JsonTypeInfo.Properties seznamu. Serializace privátních polí
Podmíněně serializovat vlastnost JsonTypeInfoKind.Object JsonPropertyInfo.ShouldSerialize Upravte predikát vlastnosti. Ignorování vlastností s konkrétním typem
Přizpůsobení zpracování čísel pro určitý typ JsonTypeInfoKind.None JsonTypeInfo.NumberHandling Upravte hodnotu pro typ. Povolit hodnotě int být řetězce

Příklad: Zvýšení hodnoty vlastnosti

Podívejte se na následující příklad, kde modifikátor zvýší hodnotu určité vlastnosti deserializace úpravou jeho JsonPropertyInfo.Set delegáta. Kromě definování modifikátoru představuje příklad také nový atribut, který používá k vyhledání vlastnosti, jejíž hodnota by měla být zvýšení. Toto je příklad přizpůsobení vlastnosti.

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

Všimněte si ve výstupu, že hodnota RoundTrips se při každé Product deserializaci instance zvýší.

Příklad: Serializace privátních polí

Ve výchozím nastavení System.Text.Json ignoruje privátní pole a vlastnosti. Tento příklad přidá nový atribut pro celou třídu, JsonIncludePrivateFieldsAttributekterý změní tuto výchozí hodnotu. Pokud modifikátor najde atribut u typu, přidá do něj všechna soukromá pole typu jako nové vlastnosti 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]
        }
    }
}

Tip

Pokud názvy privátních polí začínají podtržítky, zvažte odebrání podtržítka z názvů, když přidáte pole jako nové vlastnosti JSON.

Příklad: Ignorování vlastností s konkrétním typem

Váš model může mít vlastnosti s konkrétními názvy nebo typy, které nechcete zpřístupnit uživatelům. Můžete mít například vlastnost, která ukládá přihlašovací údaje nebo některé informace, které jsou v datové části zbytečné.

Následující příklad ukazuje, jak filtrovat vlastnosti s konkrétním typem, SecretHolder. Provede to pomocí IList<T> rozšiřující metody k odebrání všech vlastností, které mají zadaný typ ze JsonTypeInfo.Properties seznamu. Filtrované vlastnosti zcela zmizí ze kontraktu, což znamená System.Text.Json , že se na ně během serializace nebo deserializace nedívají.

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

Příklad: Povolit hodnoty int být řetězce

Váš vstupní JSON může například obsahovat uvozovky kolem jednoho z číselných typů, ale ne u ostatních. Pokud jste měli kontrolu nad třídou, mohli JsonNumberHandlingAttribute byste tento problém vyřešit tak, že ho opravíte, ale ne. Před .NET 7 byste museli napsat vlastní převaděč pro opravu tohoto chování, který vyžaduje zápis poměrně malého kódu. Pomocí přizpůsobení kontraktu můžete přizpůsobit chování zpracování čísel pro libovolný typ.

Následující příklad změní chování pro všechny int hodnoty. Příklad lze snadno upravit tak, aby platil pro jakýkoli typ nebo pro konkrétní vlastnost libovolného typu.

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

Bez modifikátoru umožňujícího čtení int hodnot z řetězce by program skončil výjimkou:

Neošetřená výjimka. System.Text.Json.JsonException: Hodnotu JSON nelze převést na System.Int32. Cesta: $. X | Číslo řádku: 0 | BytePositionInLine: 9.

Další způsoby přizpůsobení serializace

Kromě přizpůsobení kontraktu existují další způsoby, jak ovlivnit serializace a deserializační chování, včetně následujících:

Přizpůsobení kontraktu je vylepšením těchto před existujících přizpůsobení, protože možná nemáte přístup k typu pro přidání atributů. Kromě toho je zápis vlastního převaděče složitý a snižuje výkon.

Viz také