Aangepaste conversieprogramma's schrijven voor JSON-serialisatie (marshalling) in .NET

In dit artikel wordt beschreven hoe u aangepaste conversieprogramma's maakt voor de JSON-serialisatieklassen die zijn opgegeven in de System.Text.Json naamruimte. Zie JSON serialiseren en deserialiseren in .NET voor een inleiding System.Text.Jsontot.

Een conversieprogramma is een klasse die een object of een waarde converteert naar en van JSON. De System.Text.Json naamruimte heeft ingebouwde conversieprogramma's voor de meeste primitieve typen die zijn toegewezen aan JavaScript-primitieven. U kunt aangepaste conversieprogramma's schrijven om het standaardgedrag van een ingebouwd conversieprogramma te overschrijven. Voorbeeld:

  • Mogelijk wilt u waarden DateTime weergeven met de notatie mm/dd/jjjj. ISO 8601-1:2019 wordt standaard ondersteund, inclusief het RFC 3339-profiel. Zie de ondersteuning voor DateTime en DateTimeOffset in System.Text.Jsonvoor meer informatie.
  • Mogelijk wilt u een POCO serialiseren als JSON-tekenreeks, bijvoorbeeld met een PhoneNumber type.

U kunt ook aangepaste conversieprogramma's schrijven om aan te passen of uit te breiden System.Text.Json met nieuwe functionaliteit. De volgende scenario's worden verderop in dit artikel behandeld:

Visual Basic kan niet worden gebruikt om aangepaste conversieprogramma's te schrijven, maar kan conversieprogramma's aanroepen die zijn geïmplementeerd in C#-bibliotheken. Zie Visual Basic-ondersteuning voor meer informatie.

Aangepaste conversiepatronen

Er zijn twee patronen voor het maken van een aangepast conversieprogramma: het basispatroon en het fabriekspatroon. Het fabriekspatroon is bedoeld voor conversieprogramma's die type Enum of open generics verwerken. Het basispatroon is bedoeld voor niet-algemene en gesloten algemene typen. Voor conversieprogramma's voor de volgende typen is bijvoorbeeld het fabriekspatroon vereist:

Enkele voorbeelden van typen die kunnen worden verwerkt door het basispatroon zijn:

  • Dictionary<int, string>
  • WeekdaysEnum
  • List<DateTimeOffset>
  • DateTime
  • Int32

Met het basispatroon wordt een klasse gemaakt die één type kan verwerken. Het factory-patroon maakt een klasse die bepaalt, tijdens runtime, welk specifiek type vereist is en dynamisch het juiste conversieprogramma maakt.

Voorbeeld van eenvoudige conversieprogramma

Het volgende voorbeeld is een conversieprogramma dat de standaardserialisatie voor een bestaand gegevenstype overschrijft. Het conversieprogramma maakt gebruik van mm-dd-/jjjj-indeling voor DateTimeOffset eigenschappen.

using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class DateTimeOffsetJsonConverter : JsonConverter<DateTimeOffset>
    {
        public override DateTimeOffset Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) =>
                DateTimeOffset.ParseExact(reader.GetString()!,
                    "MM/dd/yyyy", CultureInfo.InvariantCulture);

        public override void Write(
            Utf8JsonWriter writer,
            DateTimeOffset dateTimeValue,
            JsonSerializerOptions options) =>
                writer.WriteStringValue(dateTimeValue.ToString(
                    "MM/dd/yyyy", CultureInfo.InvariantCulture));
    }
}

Voorbeeld van factorypatroonconversieprogramma

De volgende code toont een aangepast conversieprogramma waarmee wordt gebruikt Dictionary<Enum,TValue>. De code volgt het factory-patroon omdat de eerste algemene typeparameter is Enum en de tweede is geopend. De CanConvert methode retourneert true alleen voor een Dictionary met twee algemene parameters, waarvan de eerste een Enum type is. Het binnenste conversieprogramma krijgt een bestaand conversieprogramma voor het afhandelen van het type dat tijdens de uitvoering TValuewordt opgegeven.

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

namespace SystemTextJsonSamples
{
    public class DictionaryTKeyEnumTValueConverter : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
        {
            if (!typeToConvert.IsGenericType)
            {
                return false;
            }

            if (typeToConvert.GetGenericTypeDefinition() != typeof(Dictionary<,>))
            {
                return false;
            }

            return typeToConvert.GetGenericArguments()[0].IsEnum;
        }

        public override JsonConverter CreateConverter(
            Type type,
            JsonSerializerOptions options)
        {
            Type[] typeArguments = type.GetGenericArguments();
            Type keyType = typeArguments[0];
            Type valueType = typeArguments[1];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(DictionaryEnumConverterInner<,>).MakeGenericType(
                    [keyType, valueType]),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: [options],
                culture: null)!;

            return converter;
        }

        private class DictionaryEnumConverterInner<TKey, TValue> :
            JsonConverter<Dictionary<TKey, TValue>> where TKey : struct, Enum
        {
            private readonly JsonConverter<TValue> _valueConverter;
            private readonly Type _keyType;
            private readonly Type _valueType;

            public DictionaryEnumConverterInner(JsonSerializerOptions options)
            {
                // For performance, use the existing converter.
                _valueConverter = (JsonConverter<TValue>)options
                    .GetConverter(typeof(TValue));

                // Cache the key and value types.
                _keyType = typeof(TKey);
                _valueType = typeof(TValue);
            }

            public override Dictionary<TKey, TValue> Read(
                ref Utf8JsonReader reader,
                Type typeToConvert,
                JsonSerializerOptions options)
            {
                if (reader.TokenType != JsonTokenType.StartObject)
                {
                    throw new JsonException();
                }

                var dictionary = new Dictionary<TKey, TValue>();

                while (reader.Read())
                {
                    if (reader.TokenType == JsonTokenType.EndObject)
                    {
                        return dictionary;
                    }

                    // Get the key.
                    if (reader.TokenType != JsonTokenType.PropertyName)
                    {
                        throw new JsonException();
                    }

                    string? propertyName = reader.GetString();

                    // For performance, parse with ignoreCase:false first.
                    if (!Enum.TryParse(propertyName, ignoreCase: false, out TKey key) &&
                        !Enum.TryParse(propertyName, ignoreCase: true, out key))
                    {
                        throw new JsonException(
                            $"Unable to convert \"{propertyName}\" to Enum \"{_keyType}\".");
                    }

                    // Get the value.
                    reader.Read();
                    TValue value = _valueConverter.Read(ref reader, _valueType, options)!;

                    // Add to dictionary.
                    dictionary.Add(key, value);
                }

                throw new JsonException();
            }

            public override void Write(
                Utf8JsonWriter writer,
                Dictionary<TKey, TValue> dictionary,
                JsonSerializerOptions options)
            {
                writer.WriteStartObject();

                foreach ((TKey key, TValue value) in dictionary)
                {
                    string propertyName = key.ToString();
                    writer.WritePropertyName
                        (options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName);

                    _valueConverter.Write(writer, value, options);
                }

                writer.WriteEndObject();
            }
        }
    }
}

Stappen om het basispatroon te volgen

In de volgende stappen wordt uitgelegd hoe u een conversieprogramma maakt door het basispatroon te volgen:

  • Maak een klasse die is afgeleid van JsonConverter<T> waar T het type moet worden geserialiseerd en gedeserialiseerd.
  • Overschrijf de Read methode voor het deserialiseren van de binnenkomende JSON en converteer deze naar type T. Gebruik de Utf8JsonReader methode die wordt doorgegeven aan de methode om de JSON te lezen. U hoeft zich geen zorgen te maken over het verwerken van gedeeltelijke gegevens, omdat de serializer alle gegevens voor het huidige JSON-bereik doorgeeft. Het is dus niet nodig om aan te roepen Skip of TrySkip om te valideren dat Read retourneert true.
  • Overschrijf de Write methode om het binnenkomende object van het type Tte serialiseren. Gebruik de Utf8JsonWriter methode die wordt doorgegeven om de JSON te schrijven.
  • Overschrijf de CanConvert methode alleen indien nodig. De standaard implementatie retourneert true wanneer het type van het type Tis. Daarom hoeven conversieprogramma's die alleen het type T ondersteunen, deze methode niet te overschrijven. Zie de sectie polymorfische deserialisatie verderop in dit artikel voor een voorbeeld van een conversieprogramma dat deze methode moet overschrijven.

U kunt verwijzen naar de ingebouwde broncode voor conversieprogramma's als referentie-implementaties voor het schrijven van aangepaste conversieprogramma's.

Stappen om het factory-patroon te volgen

In de volgende stappen wordt uitgelegd hoe u een conversieprogramma maakt door het fabriekspatroon te volgen:

  • Maak een klasse die is afgeleid van JsonConverterFactory.
  • Overschrijf de CanConvert methode die moet worden geretourneerd true wanneer het type dat moet worden geconverteerd een methode is die door het conversieprogramma kan worden verwerkt. Als het conversieprogramma bijvoorbeeld bedoeld is voor List<T>, kan het alleen worden verwerkt List<int>, List<string>en List<DateTime>.
  • Overschrijf de CreateConverter methode om een exemplaar van een conversieklasse te retourneren waarmee het type-naar-conversie wordt verwerkt dat tijdens runtime wordt geleverd.
  • Maak de conversieklasse die door de CreateConverter methode wordt geïnstitueert.

Het factory-patroon is vereist voor open generics, omdat de code voor het converteren van een object naar en van een tekenreeks niet hetzelfde is voor alle typen. Een conversieprogramma voor een open algemeen type (List<T>bijvoorbeeld) moet achter de schermen een conversieprogramma maken voor een gesloten algemeen type (List<DateTime>bijvoorbeeld). Code moet worden geschreven om elk gesloten algemeen type te verwerken dat het conversieprogramma kan verwerken.

Het Enum type is vergelijkbaar met een open algemeen type: achter de schermen moet een conversieprogramma voor een specifiek Enum (WeekdaysEnumbijvoorbeeld) conversieprogramma Enum worden gemaakt.

Het gebruik van Utf8JsonReader in de Read methode

Als uw conversieprogramma een JSON-object converteert, wordt het Utf8JsonReader geplaatst op het beginobjecttoken wanneer de Read methode begint. Vervolgens moet u alle tokens in dat object lezen en de methode afsluiten met de lezer op het bijbehorende eindobjecttoken. Als u verder leest dan het einde van het object of als u stopt voordat u het bijbehorende eindtoken bereikt, krijgt u een JsonException uitzondering die aangeeft dat:

Het conversieprogramma 'ConverterName' leest te veel of niet genoeg.

Zie het bovenstaande voorbeeldconversieprogramma voor fabriekspatronen voor een voorbeeld. De Read methode begint door te controleren of de lezer op een beginobjecttoken is geplaatst. Het wordt gelezen totdat wordt gevonden dat het op het volgende eindpuntobjecttoken wordt geplaatst. Het stopt op het volgende eindobjecttoken omdat er geen tussenliggende beginobjecttokens zijn die een object binnen het object aangeven. Dezelfde regel over het begintoken en het eindtoken is van toepassing als u een matrix converteert. Zie het Stack<T> voorbeeldconversieprogramma verderop in dit artikel voor een voorbeeld.

Foutafhandeling

De serializer biedt speciale verwerking voor uitzonderingstypen JsonException en NotSupportedException.

JsonException

Als u een JsonException bericht zonder bericht genereert, wordt met de serializer een bericht gemaakt dat het pad naar het deel van de JSON bevat dat de fout heeft veroorzaakt. De instructie throw new JsonException() produceert bijvoorbeeld een foutbericht zoals in het volgende voorbeeld:

Unhandled exception. System.Text.Json.JsonException:
The JSON value could not be converted to System.Object.
Path: $.Date | LineNumber: 1 | BytePositionInLine: 37.

Als u wel een bericht opgeeft (bijvoorbeeld throw new JsonException("Error occurred")), stelt de serializer nog steeds de Path, LineNumberen BytePositionInLine eigenschappen in.

NotSupportedException

Als u een NotSupportedExceptionbericht genereert, krijgt u altijd de padinformatie in het bericht. Als u een bericht opgeeft, wordt de padgegevens eraan toegevoegd. De instructie throw new NotSupportedException("Error occurred.") produceert bijvoorbeeld een foutbericht zoals in het volgende voorbeeld:

Error occurred. The unsupported member type is located on type
'System.Collections.Generic.Dictionary`2[Samples.SummaryWords,System.Int32]'.
Path: $.TemperatureRanges | LineNumber: 4 | BytePositionInLine: 24

Wanneer te gooien welk uitzonderingstype

Wanneer de JSON-nettolading tokens bevat die niet geldig zijn voor het type dat wordt gedeserialiseerd, genereert u een JsonException.

Als je bepaalde typen niet wilt weigeren, gooi dan een NotSupportedException. Deze uitzondering is wat de serializer automatisch genereert voor typen die niet worden ondersteund. Wordt bijvoorbeeld System.Type om veiligheidsredenen niet ondersteund, dus een poging om deze te deserialiseren resulteert in een NotSupportedException.

U kunt indien nodig andere uitzonderingen genereren, maar ze bevatten niet automatisch informatie over het JSON-pad.

Een aangepast conversieprogramma registreren

Registreer een aangepast conversieprogramma om de Serialize en Deserialize methoden te gebruiken. Kies een van de volgende methoden:

Registratievoorbeeld - Verzameling conversieprogramma's

Hier volgt een voorbeeld waarmee de DateTimeOffsetJsonConverter de standaardinstelling is voor eigenschappen van het type DateTimeOffset:

var serializeOptions = new JsonSerializerOptions
{
    WriteIndented = true,
    Converters =
    {
        new DateTimeOffsetJsonConverter()
    }
};

jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

Stel dat u een exemplaar van het volgende type serialiseert:

public class WeatherForecast
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
}

Hier volgt een voorbeeld van JSON-uitvoer waarin wordt weergegeven dat het aangepaste conversieprogramma is gebruikt:

{
  "Date": "08/01/2019",
  "TemperatureCelsius": 25,
  "Summary": "Hot"
}

De volgende code maakt gebruik van dezelfde methode om het deserialiseren te deserialiseren met behulp van het aangepaste DateTimeOffset conversieprogramma:

var deserializeOptions = new JsonSerializerOptions();
deserializeOptions.Converters.Add(new DateTimeOffsetJsonConverter());
weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString, deserializeOptions)!;

Registratievoorbeeld - [JsonConverter] op een eigenschap

De volgende code selecteert een aangepast conversieprogramma voor de Date eigenschap:

public class WeatherForecastWithConverterAttribute
{
    [JsonConverter(typeof(DateTimeOffsetJsonConverter))]
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
}

Voor de code die moet worden geserialiseerd WeatherForecastWithConverterAttribute , is het gebruik van JsonSerializeOptions.Converters:

var serializeOptions = new JsonSerializerOptions
{
    WriteIndented = true
};
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

De code die moet worden gedeserialiseerd, vereist ook niet het gebruik van Converters:

weatherForecast = JsonSerializer.Deserialize<WeatherForecastWithConverterAttribute>(jsonString)!;

Registratievoorbeeld - [JsonConverter] op een type

Hier volgt code waarmee een struct wordt gemaakt en het [JsonConverter] kenmerk hierop wordt toegepast:

using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    [JsonConverter(typeof(TemperatureConverter))]
    public struct Temperature
    {
        public Temperature(int degrees, bool celsius)
        {
            Degrees = degrees;
            IsCelsius = celsius;
        }

        public int Degrees { get; }
        public bool IsCelsius { get; }
        public bool IsFahrenheit => !IsCelsius;

        public override string ToString() =>
            $"{Degrees}{(IsCelsius ? "C" : "F")}";

        public static Temperature Parse(string input)
        {
            int degrees = int.Parse(input.Substring(0, input.Length - 1));
            bool celsius = input.Substring(input.Length - 1) == "C";

            return new Temperature(degrees, celsius);
        }
    }
}

Dit is het aangepaste conversieprogramma voor de voorgaande struct:

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

namespace SystemTextJsonSamples
{
    public class TemperatureConverter : JsonConverter<Temperature>
    {
        public override Temperature Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) =>
                Temperature.Parse(reader.GetString()!);

        public override void Write(
            Utf8JsonWriter writer,
            Temperature temperature,
            JsonSerializerOptions options) =>
                writer.WriteStringValue(temperature.ToString());
    }
}

Het [JsonConverter] kenmerk in de struct registreert het aangepaste conversieprogramma als de standaardwaarde voor eigenschappen van het type Temperature. Het conversieprogramma wordt automatisch gebruikt voor de TemperatureCelsius eigenschap van het volgende type wanneer u deze serialiseert of deserialiseert:

public class WeatherForecastWithTemperatureStruct
{
    public DateTimeOffset Date { get; set; }
    public Temperature TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
}

Prioriteit van conversieprogrammaregistratie

Tijdens serialisatie of deserialisatie wordt een conversieprogramma gekozen voor elk JSON-element in de volgende volgorde, weergegeven van hoogste prioriteit tot laagste:

  • [JsonConverter] toegepast op een eigenschap.
  • Er is een conversieprogramma toegevoegd aan de Converters verzameling.
  • [JsonConverter] toegepast op een aangepast waardetype of POCO.

Als er meerdere aangepaste conversieprogramma's voor een type zijn geregistreerd in de Converters verzameling, wordt het eerste conversieprogramma gebruikt dat wordt geretourneerd trueCanConvert .

Een ingebouwd conversieprogramma wordt alleen gekozen als er geen toepasselijke aangepaste conversieprogramma is geregistreerd.

Conversieprogrammavoorbeelden voor veelvoorkomende scenario's

De volgende secties bevatten conversieprogrammavoorbeelden die betrekking hebben op enkele veelvoorkomende scenario's die niet worden verwerkt met ingebouwde functionaliteit.

Zie Ondersteunde verzamelingstypen voor een voorbeeldconversieprogrammaDataTable.

Uitgestelde typen deserialiseren naar objecteigenschappen

Wanneer een eigenschap van het type objectwordt gedeserialiseerd, wordt er een JsonElement object gemaakt. De reden hiervoor is dat de deserializer niet weet welk CLR-type moet worden gemaakt en niet probeert te raden. Als een JSON-eigenschap bijvoorbeeld 'true' heeft, wordt door de deserializer niet afgeleid dat de waarde een Boolean, en als een element '01/01/2019' heeft, wordt door de deserializer niet afgeleid dat het een DateTime.

Typedeductie kan onnauwkeurig zijn. Als de deserializer een JSON-getal parseert dat geen decimaalteken heeft als een long, kan dit leiden tot problemen buiten het bereik als de waarde oorspronkelijk is geserialiseerd als een ulong of BigInteger. Het parseren van een getal met een decimaalteken als een double getal kan de precisie verliezen als het getal oorspronkelijk is geserialiseerd als een decimal.

Voor scenario's waarvoor typedeductie is vereist, toont de volgende code een aangepast conversieprogramma voor object eigenschappen. De code converteert:

  • true en false naar Boolean
  • Getallen zonder een decimaal getal tot long
  • Getallen met een decimaal getal tot double
  • Datums tot DateTime
  • Tekenreeksen naar string
  • Alles wat u nog meer wilt JsonElement
using System.Text.Json;
using System.Text.Json.Serialization;

namespace CustomConverterInferredTypesToObject
{
    public class ObjectToInferredTypesConverter : JsonConverter<object>
    {
        public override object Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) => reader.TokenType switch
            {
                JsonTokenType.True => true,
                JsonTokenType.False => false,
                JsonTokenType.Number when reader.TryGetInt64(out long l) => l,
                JsonTokenType.Number => reader.GetDouble(),
                JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime) => datetime,
                JsonTokenType.String => reader.GetString()!,
                _ => JsonDocument.ParseValue(ref reader).RootElement.Clone()
            };

        public override void Write(
            Utf8JsonWriter writer,
            object objectToWrite,
            JsonSerializerOptions options) =>
            JsonSerializer.Serialize(writer, objectToWrite, objectToWrite.GetType(), options);
    }

    public class WeatherForecast
    {
        public object? Date { get; set; }
        public object? TemperatureCelsius { get; set; }
        public object? Summary { get; set; }
    }

    public class Program
    {
        public static void Main()
        {
            string jsonString = """
                {
                  "Date": "2019-08-01T00:00:00-07:00",
                  "TemperatureCelsius": 25,
                  "Summary": "Hot"
                }
                """;

            WeatherForecast weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString)!;
            Console.WriteLine($"Type of Date property   no converter = {weatherForecast.Date!.GetType()}");

            var options = new JsonSerializerOptions();
            options.WriteIndented = true;
            options.Converters.Add(new ObjectToInferredTypesConverter());
            weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString, options)!;
            Console.WriteLine($"Type of Date property with converter = {weatherForecast.Date!.GetType()}");

            Console.WriteLine(JsonSerializer.Serialize(weatherForecast, options));
        }
    }
}

// Produces output like the following example:
//
//Type of Date property   no converter = System.Text.Json.JsonElement
//Type of Date property with converter = System.DateTime
//{
//  "Date": "2019-08-01T00:00:00-07:00",
//  "TemperatureCelsius": 25,
//  "Summary": "Hot"
//}

In het voorbeeld ziet u de conversiecode en een WeatherForecast klasse met object eigenschappen. Met Main de methode wordt een JSON-tekenreeks gedeserialiseerd in een WeatherForecast exemplaar, eerst zonder het conversieprogramma te gebruiken en vervolgens het conversieprogramma te gebruiken. De console-uitvoer laat zien dat zonder het conversieprogramma het runtimetype voor de Date eigenschap is JsonElement; met het conversieprogramma is het runtimetype DateTime.

De map eenheidstests in de System.Text.Json.Serialization naamruimte bevat meer voorbeelden van aangepaste conversieprogramma's die deserialisatie naar object eigenschappen verwerken.

Ondersteuning voor polymorfe deserialisatie

.NET 7 biedt ondersteuning voor zowel polymorfe serialisatie als deserialisatie. In eerdere .NET-versies was er echter beperkte ondersteuning voor polymorfe serialisatie en geen ondersteuning voor deserialisatie. Als u .NET 6 of een eerdere versie gebruikt, is voor deserialisatie een aangepast conversieprogramma vereist.

Stel dat u een Person abstracte basisklasse hebt met Employee en Customer afgeleide klassen. Polymorf deserialisatie betekent dat u tijdens het ontwerp kunt opgeven Person als het deserialisatiedoel en Customer dat Employee objecten in de JSON correct worden gedeserialiseerd tijdens runtime. Tijdens de deserialisatie moet u aanwijzingen vinden die het vereiste type in de JSON identificeren. De soorten aanwijzingen die beschikbaar zijn, variëren per scenario. Een discriminerende eigenschap kan bijvoorbeeld beschikbaar zijn of u moet afhankelijk zijn van de aanwezigheid of afwezigheid van een bepaalde eigenschap. De huidige release van System.Text.Json bevat geen kenmerken om op te geven hoe polymorfe deserialisatiescenario's moeten worden verwerkt, zodat aangepaste conversieprogramma's vereist zijn.

De volgende code toont een basisklasse, twee afgeleide klassen en een aangepast conversieprogramma. Het conversieprogramma gebruikt een discriminatoreigenschap om polymorfische deserialisatie uit te voeren. Het typediscriminatie bevindt zich niet in de klassedefinities, maar wordt gemaakt tijdens serialisatie en wordt gelezen tijdens deserialisatie.

Belangrijk

Voor de voorbeeldcode moeten JSON-objectnaam-waardeparen op volgorde blijven staan. Dit is geen standaardvereiste van JSON.

public class Person
{
    public string? Name { get; set; }
}

public class Customer : Person
{
    public decimal CreditLimit { get; set; }
}

public class Employee : Person
{
    public string? OfficeNumber { get; set; }
}
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class PersonConverterWithTypeDiscriminator : JsonConverter<Person>
    {
        enum TypeDiscriminator
        {
            Customer = 1,
            Employee = 2
        }

        public override bool CanConvert(Type typeToConvert) =>
            typeof(Person).IsAssignableFrom(typeToConvert);

        public override Person Read(
            ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.StartObject)
            {
                throw new JsonException();
            }

            reader.Read();
            if (reader.TokenType != JsonTokenType.PropertyName)
            {
                throw new JsonException();
            }

            string? propertyName = reader.GetString();
            if (propertyName != "TypeDiscriminator")
            {
                throw new JsonException();
            }

            reader.Read();
            if (reader.TokenType != JsonTokenType.Number)
            {
                throw new JsonException();
            }

            TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
            Person person = typeDiscriminator switch
            {
                TypeDiscriminator.Customer => new Customer(),
                TypeDiscriminator.Employee => new Employee(),
                _ => throw new JsonException()
            };

            while (reader.Read())
            {
                if (reader.TokenType == JsonTokenType.EndObject)
                {
                    return person;
                }

                if (reader.TokenType == JsonTokenType.PropertyName)
                {
                    propertyName = reader.GetString();
                    reader.Read();
                    switch (propertyName)
                    {
                        case "CreditLimit":
                            decimal creditLimit = reader.GetDecimal();
                            ((Customer)person).CreditLimit = creditLimit;
                            break;
                        case "OfficeNumber":
                            string? officeNumber = reader.GetString();
                            ((Employee)person).OfficeNumber = officeNumber;
                            break;
                        case "Name":
                            string? name = reader.GetString();
                            person.Name = name;
                            break;
                    }
                }
            }

            throw new JsonException();
        }

        public override void Write(
            Utf8JsonWriter writer, Person person, JsonSerializerOptions options)
        {
            writer.WriteStartObject();

            if (person is Customer customer)
            {
                writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.Customer);
                writer.WriteNumber("CreditLimit", customer.CreditLimit);
            }
            else if (person is Employee employee)
            {
                writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.Employee);
                writer.WriteString("OfficeNumber", employee.OfficeNumber);
            }

            writer.WriteString("Name", person.Name);

            writer.WriteEndObject();
        }
    }
}

Met de volgende code wordt het conversieprogramma geregistreerd:

var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new PersonConverterWithTypeDiscriminator());

Het conversieprogramma kan JSON deserialiseren die is gemaakt met behulp van hetzelfde conversieprogramma om te serialiseren, bijvoorbeeld:

[
  {
    "TypeDiscriminator": 1,
    "CreditLimit": 10000,
    "Name": "John"
  },
  {
    "TypeDiscriminator": 2,
    "OfficeNumber": "555-1234",
    "Name": "Nancy"
  }
]

De conversiecode in het voorgaande voorbeeld leest en schrijft elke eigenschap handmatig. Een alternatief is het bellen Deserialize of Serialize doen van een deel van het werk. Zie dit StackOverflow-bericht voor een voorbeeld.

Een alternatieve manier om polymorfe deserialisatie uit te voeren

U kunt de Read methode aanroepenDeserialize:

  • Maak een kloon van het Utf8JsonReader exemplaar. Omdat Utf8JsonReader dit een struct is, is hiervoor alleen een toewijzingsinstructie vereist.
  • Gebruik de kloon om de discriminatortokens te lezen.
  • Roep Deserialize het oorspronkelijke Reader exemplaar aan wanneer u weet welk type u nodig hebt. U kunt aanroepen Deserialize omdat het oorspronkelijke Reader exemplaar nog steeds is geplaatst om het beginobjecttoken te lezen.

Een nadeel van deze methode is dat u niet kunt doorgeven in het oorspronkelijke optiesexemplaren waarop het conversieprogramma Deserializewordt geregistreerd. Als u dit doet, wordt een stack-overloop veroorzaakt, zoals wordt uitgelegd in de vereiste eigenschappen. In het volgende voorbeeld ziet u een Read methode die gebruikmaakt van dit alternatief:

public override Person Read(
    ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    Utf8JsonReader readerClone = reader;

    if (readerClone.TokenType != JsonTokenType.StartObject)
    {
        throw new JsonException();
    }

    readerClone.Read();
    if (readerClone.TokenType != JsonTokenType.PropertyName)
    {
        throw new JsonException();
    }

    string? propertyName = readerClone.GetString();
    if (propertyName != "TypeDiscriminator")
    {
        throw new JsonException();
    }

    readerClone.Read();
    if (readerClone.TokenType != JsonTokenType.Number)
    {
        throw new JsonException();
    }

    TypeDiscriminator typeDiscriminator = (TypeDiscriminator)readerClone.GetInt32();
    Person person = typeDiscriminator switch
    {
        TypeDiscriminator.Customer => JsonSerializer.Deserialize<Customer>(ref reader)!,
        TypeDiscriminator.Employee => JsonSerializer.Deserialize<Employee>(ref reader)!,
        _ => throw new JsonException()
    };
    return person;
}

Ondersteuning voor retouren voor Stack typen

Als u een JSON-tekenreeks deserialiseert in een Stack object en dat object vervolgens serialiseert, bevindt de inhoud van de stack zich in omgekeerde volgorde. Dit gedrag is van toepassing op de volgende typen en interfaces en door de gebruiker gedefinieerde typen die hiervan zijn afgeleid:

Ter ondersteuning van serialisatie en deserialisatie die de oorspronkelijke volgorde in de stack behoudt, is een aangepast conversieprogramma vereist.

De volgende code toont een aangepast conversieprogramma waarmee retourneert naar en van Stack<T> objecten:

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

namespace SystemTextJsonSamples
{
    public class JsonConverterFactoryForStackOfT : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
            => typeToConvert.IsGenericType
            && typeToConvert.GetGenericTypeDefinition() == typeof(Stack<>);

        public override JsonConverter CreateConverter(
            Type typeToConvert, JsonSerializerOptions options)
        {
            Debug.Assert(typeToConvert.IsGenericType &&
                typeToConvert.GetGenericTypeDefinition() == typeof(Stack<>));

            Type elementType = typeToConvert.GetGenericArguments()[0];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(JsonConverterForStackOfT<>)
                    .MakeGenericType(new Type[] { elementType }),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: null,
                culture: null)!;

            return converter;
        }
    }

    public class JsonConverterForStackOfT<T> : JsonConverter<Stack<T>>
    {
        public override Stack<T> Read(
            ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.StartArray)
            {
                throw new JsonException();
            }
            reader.Read();

            var elements = new Stack<T>();

            while (reader.TokenType != JsonTokenType.EndArray)
            {
                elements.Push(JsonSerializer.Deserialize<T>(ref reader, options)!);

                reader.Read();
            }

            return elements;
        }

        public override void Write(
            Utf8JsonWriter writer, Stack<T> value, JsonSerializerOptions options)
        {
            writer.WriteStartArray();

            var reversed = new Stack<T>(value);

            foreach (T item in reversed)
            {
                JsonSerializer.Serialize(writer, item, options);
            }

            writer.WriteEndArray();
        }
    }
}

Met de volgende code wordt het conversieprogramma geregistreerd:

var options = new JsonSerializerOptions
{
    Converters = { new JsonConverterFactoryForStackOfT() },
};

Naamgevingsbeleid voor opsommingstekenreeksdeserialisatie

Standaard kan de ingebouwde JsonStringEnumConverter tekenreekswaarden voor opsommingen serialiseren en deserialiseren. Het werkt zonder een opgegeven naamgevingsbeleid of met het CamelCase naamgevingsbeleid. Het biedt geen ondersteuning voor andere naamgevingsbeleidsregels, zoals slangenkoffers. Zie voor meer informatie over aangepaste conversieprogrammacode die ondersteuning biedt voor roundtripping naar enum-tekenreekswaarden terwijl u een naamgevingsbeleid voor slangencases gebruikt, gitHub issue dotnet/runtime #31619. U kunt ook upgraden naar .NET 7 of nieuwere versies, die ingebouwde ondersteuning bieden voor het toepassen van naamgevingsbeleid bij het afronden van en naar opsommingstekenreekswaarden.

Standaardsysteemconversieprogramma gebruiken

In sommige scenario's wilt u mogelijk het standaardsysteemconversieprogramma gebruiken in een aangepast conversieprogramma. Hiervoor haalt u het systeemconversieprogramma op uit de JsonSerializerOptions.Default eigenschap, zoals wordt weergegeven in het volgende voorbeeld:

public class MyCustomConverter : JsonConverter<int>
{
    private readonly static JsonConverter<int> s_defaultConverter = 
        (JsonConverter<int>)JsonSerializerOptions.Default.GetConverter(typeof(int));

    // Custom serialization logic
    public override void Write(
        Utf8JsonWriter writer, int value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString());
    }

    // Fall back to default deserialization logic
    public override int Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return s_defaultConverter.Read(ref reader, typeToConvert, options);
    }
}

Omgaan met null-waarden

Standaard verwerkt de serializer null-waarden als volgt:

  • Voor referentietypen en Nullable<T> -typen:

    • Het wordt niet doorgegeven null aan aangepaste conversieprogramma's bij serialisatie.
    • Het wordt niet doorgegeven JsonTokenType.Null aan aangepaste conversieprogramma's bij deserialisatie.
    • Het retourneert een null exemplaar bij deserialisatie.
    • Het schrijft null rechtstreeks met de schrijver over serialisatie.
  • Voor niet-null-waardetypen:

    • Het wordt doorgegeven JsonTokenType.Null aan aangepaste conversieprogramma's bij deserialisatie. (Als er geen aangepast conversieprogramma beschikbaar is, wordt er een JsonException uitzondering gegenereerd door het interne conversieprogramma voor het type.)

Dit gedrag voor null-verwerking is voornamelijk bedoeld om de prestaties te optimaliseren door een extra aanroep naar het conversieprogramma over te slaan. Bovendien wordt voorkomen dat conversieprogramma's voor null-typen worden afgedwongen om aan het begin van elke Read en Write methode-onderdrukking te controlerennull.

Als u wilt dat een aangepast conversieprogramma kan worden verwerkt null voor een verwijzing of waardetype, moet u overschrijven JsonConverter<T>.HandleNull om te retourneren true, zoals wordt weergegeven in het volgende voorbeeld:

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

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

        [JsonConverter(typeof(DescriptionConverter))]
        public string? Description { get; set; }
    }

    public class DescriptionConverter : JsonConverter<string>
    {
        public override bool HandleNull => true;

        public override string Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) =>
            reader.GetString() ?? "No description provided.";

        public override void Write(
            Utf8JsonWriter writer,
            string value,
            JsonSerializerOptions options) =>
            writer.WriteStringValue(value);
    }

    public class Program
    {
        public static void Main()
        {
            string json = @"{""x"":1,""y"":2,""Description"":null}";

            Point point = JsonSerializer.Deserialize<Point>(json)!;
            Console.WriteLine($"Description: {point.Description}");
        }
    }
}

// Produces output like the following example:
//
//Description: No description provided.

Verwijzingen behouden

Referentiegegevens worden standaard alleen in de cache opgeslagen voor elke aanroep naar Serialize of Deserialize. Als u verwijzingen van de ene Serialize/Deserialize aanroep naar de andere wilt behouden, moet u het ReferenceResolver exemplaar in de aanroepsite van .Serialize/Deserialize De volgende code toont een voorbeeld voor dit scenario:

  • U schrijft een aangepast conversieprogramma voor het Company type.
  • U wilt de Supervisor eigenschap niet handmatig serialiseren, een Employee. U wilt dit delegeren aan de serializer en u wilt ook de verwijzingen behouden die u al hebt opgeslagen.

Dit zijn de Employee en Company klassen:

public class Employee
{
    public string? Name { get; set; }
    public Employee? Manager { get; set; }
    public List<Employee>? DirectReports { get; set; }
    public Company? Company { get; set; }
}

public class Company
{
    public string? Name { get; set; }
    public Employee? Supervisor { get; set; }
}

Het conversieprogramma ziet er als volgt uit:

class CompanyConverter : JsonConverter<Company>
{
    public override Company Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, Company value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();

        writer.WriteString("Name", value.Name);

        writer.WritePropertyName("Supervisor");
        JsonSerializer.Serialize(writer, value.Supervisor, options);

        writer.WriteEndObject();
    }
}

Een klasse die is afgeleid van de opslag van ReferenceResolver de verwijzingen in een woordenlijst:

class MyReferenceResolver : ReferenceResolver
{
    private uint _referenceCount;
    private readonly Dictionary<string, object> _referenceIdToObjectMap = new ();
    private readonly Dictionary<object, string> _objectToReferenceIdMap = new (ReferenceEqualityComparer.Instance);

    public override void AddReference(string referenceId, object value)
    {
        if (!_referenceIdToObjectMap.TryAdd(referenceId, value))
        {
            throw new JsonException();
        }
    }

    public override string GetReference(object value, out bool alreadyExists)
    {
        if (_objectToReferenceIdMap.TryGetValue(value, out string? referenceId))
        {
            alreadyExists = true;
        }
        else
        {
            _referenceCount++;
            referenceId = _referenceCount.ToString();
            _objectToReferenceIdMap.Add(value, referenceId);
            alreadyExists = false;
        }

        return referenceId;
    }

    public override object ResolveReference(string referenceId)
    {
        if (!_referenceIdToObjectMap.TryGetValue(referenceId, out object? value))
        {
            throw new JsonException();
        }

        return value;
    }
}

Een klasse die is afgeleid van ReferenceHandler een exemplaar van MyReferenceResolver en maakt alleen een nieuw exemplaar wanneer dat nodig is (in een methode die in dit voorbeeld wordt genoemd Reset ):

class MyReferenceHandler : ReferenceHandler
{
    public MyReferenceHandler() => Reset();

    private ReferenceResolver? _rootedResolver;
    public override ReferenceResolver CreateResolver() => _rootedResolver!;
    public void Reset() => _rootedResolver = new MyReferenceResolver();
}

Wanneer de voorbeeldcode de serializer aanroept, wordt er een JsonSerializerOptions exemplaar gebruikt waarin de ReferenceHandler eigenschap is ingesteld op een exemplaar van MyReferenceHandler. Wanneer u dit patroon volgt, moet u de ReferenceResolver woordenlijst opnieuw instellen wanneer u klaar bent met serialiseren, zodat u het niet voor altijd kunt laten groeien.

var options = new JsonSerializerOptions();

options.Converters.Add(new CompanyConverter());
var myReferenceHandler = new MyReferenceHandler();
options.ReferenceHandler = myReferenceHandler;
options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.WriteIndented = true;

string str = JsonSerializer.Serialize(tyler, options);

// Reset after serializing to avoid out of bounds memory growth in the resolver.
myReferenceHandler.Reset();

In het voorgaande voorbeeld wordt alleen serialisatie uitgevoerd, maar een vergelijkbare benadering kan worden gebruikt voor deserialisatie.

Andere voorbeelden van aangepaste conversieprogramma's

Het artikel Migreren van Newtonsoft.Json naar System.Text.Json artikel bevat aanvullende voorbeelden van aangepaste conversieprogramma's.

De map eenheidstests in de System.Text.Json.Serialization broncode bevat andere aangepaste conversieprogrammavoorbeelden, zoals:

Als u een conversieprogramma wilt maken dat het gedrag van een bestaand ingebouwd conversieprogramma wijzigt, kunt u de broncode van het bestaande conversieprogramma ophalen om te fungeren als uitgangspunt voor aanpassing.

Aanvullende bronnen