Anpassa ett JSON-kontrakt

Biblioteket System.Text.Json konstruerar ett JSON-kontraktför varje .NET-typ, som definierar hur typen ska serialiseras och deserialiseras. Kontraktet härleds från typens form, som innehåller egenskaper som egenskaper och fält och om det implementerar IEnumerable gränssnittet eller IDictionary . Typer mappas till kontrakt antingen vid körning med reflektion eller vid kompilering med hjälp av källgeneratorn.

Från och med .NET 7 kan du anpassa dessa JSON-kontrakt för att ge mer kontroll över hur typer konverteras till JSON och vice versa. I följande lista visas bara några exempel på vilka typer av anpassningar du kan göra för serialisering och deserialisering:

  • Serialisera privata fält och egenskaper.
  • Stöd för flera namn för en enskild egenskap (till exempel om en tidigare biblioteksversion använde ett annat namn).
  • Ignorera egenskaper med ett specifikt namn, typ eller värde.
  • Skilja mellan explicita null värden och bristen på ett värde i JSON-nyttolasten.
  • Stödattribut System.Runtime.Serialization , till exempel DataContractAttribute. Mer information finns i Attribut för System.Runtime.Serialization.
  • Utlöser ett undantag om JSON innehåller en egenskap som inte är en del av måltypen. Mer information finns i Hantera medlemmar som saknas.

Så här anmäler du dig

Det finns två sätt att ansluta till anpassning. Båda handlar om att hämta en lösning, vars jobb är att tillhandahålla en JsonTypeInfo instans för varje typ som måste serialiseras.

Konfigurerbara aspekter

Egenskapen JsonTypeInfo.Kind anger hur konverteraren serialiserar en viss typ, till exempel som ett objekt eller som en matris, och om dess egenskaper serialiseras. Du kan fråga den här egenskapen för att avgöra vilka aspekter av en typs JSON-kontrakt som du kan konfigurera. Det finns fyra olika typer:

JsonTypeInfo.Kind beskrivning
JsonTypeInfoKind.Object Konverteraren serialiserar typen till ett JSON-objekt och använder dess egenskaper. Den här typen används för de flesta klass- och structtyper och ger mest flexibilitet.
JsonTypeInfoKind.Enumerable Konverteraren serialiserar typen till en JSON-matris. Den här typen används för typer som List<T> och matriser.
JsonTypeInfoKind.Dictionary Konverteraren serialiserar typen till ett JSON-objekt. Den här typen används för typer som Dictionary<K, V>.
JsonTypeInfoKind.None Konverteraren anger inte hur den ska serialisera typen eller vilka JsonTypeInfo egenskaper den ska använda. Den här typen används för typer som System.Object, intoch stringoch för alla typer som använder en anpassad konverterare.

Modifierare

En modifierare är en Action<JsonTypeInfo> eller en metod med en JsonTypeInfo parameter som hämtar kontraktets aktuella tillstånd som argument och gör ändringar i kontraktet. Du kan till exempel iterera genom de förifyllda egenskaperna på den angivna JsonTypeInfo för att hitta den du är intresserad av och sedan ändra dess JsonPropertyInfo.Get egenskap (för serialisering) eller JsonPropertyInfo.Set egenskap (för deserialisering). Eller så kan du skapa en ny egenskap med och JsonTypeInfo.CreateJsonPropertyInfo(Type, String) lägga till den i JsonTypeInfo.Properties samlingen.

I följande tabell visas de ändringar du kan göra och hur du uppnår dem.

Ändring Tillämpliga JsonTypeInfo.Kind Så här gör du för att uppnå det Exempel
Anpassa en egenskaps värde JsonTypeInfoKind.Object Ändra ombudet JsonPropertyInfo.Get (för serialisering) eller JsonPropertyInfo.Set ombudet (för deserialisering) för egenskapen. Öka värdet för en egenskap
Lägga till eller ta bort egenskaper JsonTypeInfoKind.Object Lägg till eller ta bort objekt från JsonTypeInfo.Properties listan. Serialisera privata fält
Serialisera en egenskap villkorligt JsonTypeInfoKind.Object Ändra predikatet JsonPropertyInfo.ShouldSerialize för egenskapen. Ignorera egenskaper med en viss typ
Anpassa nummerhantering för en viss typ JsonTypeInfoKind.None JsonTypeInfo.NumberHandling Ändra värdet för typen. Tillåt att int-värden är strängar

Exempel: Öka värdet för en egenskap

Tänk dig följande exempel där modifieraren ökar värdet för en viss egenskap vid deserialisering genom att ändra dess JsonPropertyInfo.Set ombud. Förutom att definiera modifieraren introducerar exemplet även ett nytt attribut som används för att hitta egenskapen vars värde ska ökas. Det här är ett exempel på hur du anpassar en egenskap.

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

Observera i utdata att värdet RoundTrips för ökas varje gång instansen Product deserialiseras.

Exempel: Serialisera privata fält

Som standard System.Text.Json ignorerar privata fält och egenskaper. Det här exemplet lägger till ett nytt klassomfattande attribut, JsonIncludePrivateFieldsAttribute, för att ändra standardvärdet. Om modifieraren hittar attributet för en typ lägger den till alla privata fält på typen som nya egenskaper till 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]
        }
    }
}

Dricks

Om dina privata fältnamn börjar med understreck bör du överväga att ta bort understrecken från namnen när du lägger till fälten som nya JSON-egenskaper.

Exempel: Ignorera egenskaper med en viss typ

Din modell kanske har egenskaper med specifika namn eller typer som du inte vill exponera för användare. Du kan till exempel ha en egenskap som lagrar autentiseringsuppgifter eller viss information som är värdelös att ha i nyttolasten.

I följande exempel visas hur du filtrerar bort egenskaper med en viss typ, SecretHolder. Det gör det med hjälp av en IList<T> tilläggsmetod för att ta bort alla egenskaper som har den angivna typen från JsonTypeInfo.Properties listan. De filtrerade egenskaperna försvinner helt från kontraktet, vilket innebär att System.Text.Json de inte tittar på dem under serialisering eller deserialisering.

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

Exempel: Tillåt att int-värden är strängar

Kanske kan din indata-JSON innehålla citattecken runt någon av de numeriska typerna, men inte på andra. Om du hade kontroll över klassen kan du placera JsonNumberHandlingAttribute på typen för att åtgärda detta, men det gör du inte. Innan .NET 7 skulle du behöva skriva en anpassad konverterare för att åtgärda det här beteendet, vilket kräver att du skriver en hel del kod. Med hjälp av kontraktsanpassning kan du anpassa beteendet för nummerhantering för valfri typ.

I följande exempel ändras beteendet för alla int värden. Exemplet kan enkelt justeras för att gälla för valfri typ eller för en specifik egenskap av vilken typ som helst.

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

Utan modifieraren för att tillåta läsningsvärden int från en sträng skulle programmet ha slutat med ett undantag:

Ohanterat undantag. System.Text.Json.JsonException: JSON-värdet kunde inte konverteras till System.Int32. Sökväg: $. X | LineNumber: 0 | BytePositionInLine: 9.

Andra sätt att anpassa serialisering

Förutom att anpassa ett kontrakt finns det andra sätt att påverka serialiserings- och deserialiseringsbeteende, inklusive följande:

  • Genom att använda attribut som härletts från JsonAttribute, till exempel JsonIgnoreAttribute och JsonPropertyOrderAttribute.
  • Genom att JsonSerializerOptionsändra , till exempel för att ange en namngivningsprincip eller serialisera uppräkningsvärden som strängar i stället för tal.
  • Genom att skriva en anpassad konverterare som utför det faktiska arbetet med att skriva JSON och, under deserialisering, konstruera ett objekt.

Kontraktsanpassning är en förbättring jämfört med dessa befintliga anpassningar eftersom du kanske inte har åtkomst till typen för att lägga till attribut, och att skriva en anpassad konverterare är komplext och skadar prestanda.

Se även