Så här skriver du anpassade konverterare för JSON-serialisering (marshalling) i .NET

Den här artikeln visar hur du skapar anpassade konverterare för JSON-serialiseringsklasserna som tillhandahålls i System.Text.Json namnområdet. En introduktion till System.Text.Jsonfinns i Serialisera och deserialisera JSON i .NET.

En konverterare är en klass som konverterar ett objekt eller ett värde till och från JSON. Namnområdet System.Text.Json har inbyggda konverterare för de flesta primitiva typer som mappar till JavaScript-primitiver. Du kan skriva anpassade konverterare för att åsidosätta standardbeteendet för en inbyggd konverterare. Till exempel:

  • Du kanske vill DateTime att värden ska representeras av formatet mm/dd/åååå. Som standard stöds ISO 8601-1:2019, inklusive RFC 3339-profilen. Mer information finns i Stöd för DateTime och DateTimeOffset i System.Text.Json.
  • Du kanske vill serialisera en POCO som JSON-sträng, till exempel med en PhoneNumber typ.

Du kan också skriva anpassade konverterare för att anpassa eller utöka System.Text.Json med nya funktioner. Följande scenarier beskrivs senare i den här artikeln:

Visual Basic kan inte användas för att skriva anpassade konverterare, men kan anropa konverterare som implementeras i C#-bibliotek. Mer information finns i Visual Basic-stöd.

Anpassade konverterarmönster

Det finns två mönster för att skapa en anpassad konverterare: det grundläggande mönstret och fabriksmönstret. Fabriksmönstret är för konverterare som hanterar typ Enum eller öppnar generiska objekt. Det grundläggande mönstret är för icke-generiska och stängda generiska typer. Konverterare för följande typer kräver till exempel fabriksmönstret:

Några exempel på typer som kan hanteras av det grundläggande mönstret är:

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

Det grundläggande mönstret skapar en klass som kan hantera en typ. Fabriksmönstret skapar en klass som vid körning avgör vilken specifik typ som krävs och dynamiskt skapar lämplig konverterare.

Grundläggande exempelkonverterare

Följande exempel är en konverterare som åsidosätter standard serialisering för en befintlig datatyp. Konverteraren använder formatet mm/dd/åååå för DateTimeOffset egenskaper.

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

Exempel på fabriksmönsterkonverterare

Följande kod visar en anpassad konverterare som fungerar med Dictionary<Enum,TValue>. Koden följer fabriksmönstret eftersom den första generiska typparametern är Enum och den andra är öppen. Metoden CanConvert returnerar true endast för en Dictionary med två generiska parametrar, varav den första är en Enum typ. Den inre konverteraren hämtar en befintlig konverterare för att hantera den typ som anges vid körning för TValue.

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

Steg för att följa det grundläggande mönstret

Följande steg förklarar hur du skapar en konverterare genom att följa det grundläggande mönstret:

  • Skapa en klass som härleds från JsonConverter<T> var T är den typ som ska serialiseras och deserialiseras.
  • Read Åsidosätt metoden för att deserialisera inkommande JSON och konvertera den till att skriva T. Använd den Utf8JsonReader som skickas till metoden för att läsa JSON. Du behöver inte bekymra dig om att hantera partiella data eftersom serialiseraren skickar alla data för det aktuella JSON-omfånget. Därför är det inte nödvändigt att anropa Skip eller verifiera att Read returnerar trueTrySkip .
  • Åsidosätt Write metoden för att serialisera det inkommande objektet av typen T. Använd som Utf8JsonWriter skickas till metoden för att skriva JSON.
  • Åsidosätt CanConvert metoden endast om det behövs. Standardimplementeringen returneras true när typen som ska konverteras är av typen T. Konverterare som endast stöder typen T behöver därför inte åsidosätta den här metoden. Ett exempel på en konverterare som behöver åsidosätta den här metoden finns i avsnittet polymorf deserialisering senare i den här artikeln.

Du kan referera till den inbyggda konverterarens källkod som referensimplementeringar för att skriva anpassade konverterare.

Steg för att följa fabriksmönstret

Följande steg förklarar hur du skapar en konverterare genom att följa fabriksmönstret:

  • Skapa en klass som härleds från JsonConverterFactory.
  • Åsidosätt metoden CanConvert för att returnera true när typen som ska konverteras är en som konverteraren kan hantera. Om konverteraren till exempel är för List<T>kan den bara hantera List<int>, List<string>och List<DateTime>.
  • Åsidosätt CreateConverter metoden för att returnera en instans av en konverterarklass som hanterar den typ-till-konvertera som tillhandahålls vid körning.
  • Skapa den konverterarklass som CreateConverter metoden instansierar.

Fabriksmönstret krävs för öppna generiska objekt eftersom koden för att konvertera ett objekt till och från en sträng inte är samma för alla typer. En konverterare för en öppen allmän typ (List<T>till exempel) måste skapa en konverterare för en sluten allmän typ (List<DateTime>till exempel) i bakgrunden. Koden måste skrivas för att hantera varje sluten-generisk typ som konverteraren kan hantera.

Typen Enum liknar en öppen allmän typ: en konverterare för Enum måste skapa en konverterare för en specifik Enum (WeekdaysEnumtill exempel) bakom kulisserna.

Användningen av Utf8JsonReader i Read -metoden

Om konverteraren konverterar ett JSON-objekt Utf8JsonReader placeras den på startobjekttoken när Read metoden börjar. Du måste sedan läsa igenom alla token i objektet och avsluta metoden med läsaren placerad på motsvarande slutobjekttoken. Om du läser bortom slutet av objektet, eller om du slutar innan du når motsvarande sluttoken, får du ett JsonException undantag som anger att:

Konverteraren "ConverterName" läste för mycket eller inte tillräckligt.

Ett exempel finns i föregående exempelkonverterare för fabriksmönster. Metoden Read börjar med att verifiera att läsaren är placerad på en startobjekttoken. Den läser tills den hittar att den är placerad på nästa slutobjekttoken. Den stoppas på nästa slutobjekttoken eftersom det inte finns några mellanliggande startobjekttoken som indikerar ett objekt i objektet. Samma regel om starttoken och sluttoken gäller om du konverterar en matris. Ett exempel finns i Stack<T> exempelkonverteraren senare i den här artikeln.

Felhantering

Serialiseraren tillhandahåller särskild hantering för undantagstyper JsonException och NotSupportedException.

JsonException

Om du genererar ett JsonException utan meddelande skapar serialiseraren ett meddelande som innehåller sökvägen till den del av JSON som orsakade felet. Instruktionen throw new JsonException() genererar till exempel ett felmeddelande som i följande exempel:

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

Om du anger ett meddelande (till exempel throw new JsonException("Error occurred")), anger Pathserialiseraren fortfarande egenskaperna , LineNumberoch BytePositionInLine .

NotSupportedException

Om du genererar en NotSupportedExceptionfår du alltid sökvägsinformationen i meddelandet. Om du anger ett meddelande läggs sökvägsinformationen till. Instruktionen throw new NotSupportedException("Error occurred.") genererar till exempel ett felmeddelande som i följande exempel:

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

När du ska utlösa vilken undantagstyp

När JSON-nyttolasten innehåller token som inte är giltiga för den typ som deserialiseras genererar du en JsonException.

När du inte vill tillåta vissa typer genererar du en NotSupportedException. Det här undantaget är vad serialiseraren automatiskt genererar för typer som inte stöds. Till exempel System.Type stöds inte av säkerhetsskäl, så ett försök att deserialisera det resulterar i en NotSupportedException.

Du kan utlösa andra undantag efter behov, men de innehåller inte automatiskt JSON-sökvägsinformation.

Registrera en anpassad konverterare

Registrera en anpassad konverterare för att få Serialize metoderna och Deserialize att använda den. Välj någon av följande metoder:

Registreringsexempel – Konverterarsamling

Här är ett exempel som gör DateTimeOffsetJsonConverter till standard för egenskaper av typen DateTimeOffset:

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

jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

Anta att du serialiserar en instans av följande typ:

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

Här är ett exempel på JSON-utdata som visar att den anpassade konverteraren användes:

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

Följande kod använder samma metod för att deserialisera med den anpassade DateTimeOffset konverteraren:

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

Registreringsexempel – [JsonConverter] på en egenskap

Följande kod väljer en anpassad konverterare för Date egenskapen:

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

Koden för serialisering WeatherForecastWithConverterAttribute kräver inte användning av JsonSerializeOptions.Converters:

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

Koden för att deserialisera kräver inte heller användning av Converters:

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

Registreringsexempel – [JsonConverter] på en typ

Här är kod som skapar en struct och tillämpar [JsonConverter] attributet på den:

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

Här är den anpassade konverteraren för föregående 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());
    }
}

Attributet [JsonConverter] på struct registrerar den anpassade konverteraren som standard för egenskaper av typen Temperature. Konverteraren används automatiskt på TemperatureCelsius egenskapen av följande typ när du serialiserar eller deserialiserar den:

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

Registreringspriorence för konverterare

Under serialisering eller deserialisering väljs en konverterare för varje JSON-element i följande ordning, som anges från högsta prioritet till lägsta:

  • [JsonConverter] tillämpas på en egenskap.
  • En konverterare har lagts till i Converters samlingen.
  • [JsonConverter] tillämpas på en anpassad värdetyp eller POCO.

Om flera anpassade konverterare för en typ registreras i Converters samlingen används den första konverteraren som returneras true för CanConvert .

En inbyggd konverterare väljs endast om ingen tillämplig anpassad konverterare har registrerats.

Konverterarexempel för vanliga scenarier

Följande avsnitt innehåller konverterarexempel som hanterar några vanliga scenarier som inbyggda funktioner inte hanterar.

En exempelkonverterare DataTable finns i Samlingstyper som stöds.

Deserialisera uppskjutna typer till objektegenskaper

När deserialisera till en egenskap av typen objectskapas ett JsonElement objekt. Anledningen är att deserialiseraren inte vet vilken CLR-typ som ska skapas, och den försöker inte gissa. Om en JSON-egenskap till exempel har "true" kommer deserialiseraren inte att dra slutsatsen att värdet är en Boolean, och om ett element har "01/01/2019" kommer deserialiseraren inte att dra slutsatsen att det är en DateTime.

Typinferens kan vara felaktig. Om deserialiseraren parsar ett JSON-tal som inte har någon decimalpunkt som en long, kan det resultera i out-of-range-problem om värdet ursprungligen serialiserades som en ulong eller BigInteger. Parsning av ett tal som har en decimalpunkt som en double kan förlora precision om talet ursprungligen serialiserades som en decimal.

För scenarier som kräver typinferens visar följande kod en anpassad konverterare för object egenskaper. Koden konverterar:

  • true och false för att Boolean
  • Tal utan decimal till long
  • Tal med decimaler till double
  • Datum till DateTime
  • Strängar till string
  • Allt annat att 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"
//}

Exemplet visar konverterarkoden och en WeatherForecast klass med object egenskaper. Metoden Main deserialiserar en JSON-sträng till en WeatherForecast instans, först utan att använda konverteraren och sedan med konverteraren. Konsolens utdata visar att utan konverteraren är JsonElementkörningstypen för Date egenskapen . Med konverteraren är DateTimekörningstypen .

Mappen enhetstest i System.Text.Json.Serialization namnområdet innehåller fler exempel på anpassade konverterare som hanterar deserialisering till object egenskaper.

Stöd för polymorf deserialisering

.NET 7 har stöd för både polymorf serialisering och deserialisering. I tidigare .NET-versioner fanns det dock begränsat stöd för polymorf serialisering och inget stöd för deserialisering. Om du använder .NET 6 eller en tidigare version kräver deserialisering en anpassad konverterare.

Anta till exempel att du har en Person abstrakt basklass med Employee och Customer härledda klasser. Polymorf deserialisering innebär att du vid designtillfället kan ange Person som mål för deserialisering och Customer att Employee objekt i JSON är korrekt deserialiserade vid körning. Under deserialiseringen måste du hitta ledtrådar som identifierar den typ som krävs i JSON. Vilka typer av ledtrådar som är tillgängliga varierar med varje scenario. Till exempel kan en diskriminerande egenskap vara tillgänglig eller så kan du behöva förlita dig på förekomsten eller frånvaron av en viss egenskap. Den aktuella versionen av System.Text.Json innehåller inte attribut för att ange hur polymorfa deserialiseringsscenarier ska hanteras, så anpassade konverterare krävs.

Följande kod visar en basklass, två härledda klasser och en anpassad konverterare för dem. Konverteraren använder en diskriminerande egenskap för att utföra polymorf deserialisering. Typdiskriminatorn finns inte i klassdefinitionerna utan skapas under serialiseringen och läss under deserialiseringen.

Viktigt!

Exempelkoden kräver JSON-objektnamn/värdepar för att hålla sig i ordning, vilket inte är ett standardkrav för 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();
        }
    }
}

Följande kod registrerar konverteraren:

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

Konverteraren kan deserialisera JSON som skapades med hjälp av samma konverterare för att serialisera, till exempel:

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

Konverterarkoden i föregående exempel läser och skriver varje egenskap manuellt. Ett alternativ är att anropa Deserialize eller Serialize utföra en del av arbetet. Ett exempel finns i det här StackOverflow-inlägget.

Ett alternativt sätt att utföra polymorf deserialisering

Du kan anropa Deserialize metoden Read :

  • Skapa en klon av instansen Utf8JsonReader . Eftersom Utf8JsonReader är en struct kräver detta bara en tilldelningsinstruktuering.
  • Använd klonen för att läsa igenom diskriminerande token.
  • Anropa Deserialize med den ursprungliga Reader instansen när du vet vilken typ du behöver. Du kan anropa Deserialize eftersom den ursprungliga Reader instansen fortfarande är placerad för att läsa startobjekttoken.

En nackdel med den här metoden är att du inte kan skicka in den ursprungliga alternativinstansen som registrerar konverteraren till Deserialize. Detta skulle orsaka ett stackspill, enligt beskrivningen i Obligatoriska egenskaper. I följande exempel visas en Read metod som använder det här alternativet:

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

Support tur och retur för Stack typer

Om du deserialiserar en JSON-sträng till ett Stack objekt och sedan serialiserar objektet är innehållet i stacken i omvänd ordning. Det här beteendet gäller för följande typer och gränssnitt och användardefinierade typer som härleds från dem:

För att stödja serialisering och deserialisering som behåller den ursprungliga ordningen i stacken krävs en anpassad konverterare.

Följande kod visar en anpassad konverterare som möjliggör rund-tripping till och från Stack<T> objekt:

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

Följande kod registrerar konverteraren:

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

Namngivningsprinciper för uppräkningssträngsdeserialisering

Som standard kan de inbyggda JsonStringEnumConverter serialisera och deserialisera strängvärden för uppräkningar. Den fungerar utan en angiven namngivningsprincip eller med CamelCase namngivningsprincipen. Det stöder inte andra namngivningsprinciper, till exempel ormfall. Information om anpassad konverterarkod som kan stödja rund-tripping till och från uppräkningssträngsvärden när du använder en namngivningsprincip för ormfall finns i GitHub-problem med dotnet/runtime #31619. Du kan också uppgradera till .NET 7 eller senare versioner, som ger inbyggt stöd för att tillämpa namngivningsprinciper vid avrundning till och från uppräkningssträngsvärden.

Använda standardsystemkonverterare

I vissa scenarier kanske du vill använda standardsystemkonverteraren i en anpassad konverterare. Det gör du genom att hämta systemkonverteraren från JsonSerializerOptions.Default egenskapen, som du ser i följande exempel:

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

Hantera nullvärden

Som standard hanterar serialiseraren null-värden på följande sätt:

  • För referenstyper och Nullable<T> typer:

    • Den skickas null inte till anpassade konverterare vid serialisering.
    • Den skickas JsonTokenType.Null inte till anpassade konverterare vid deserialisering.
    • Den returnerar en null instans vid deserialisering.
    • Den skriver null direkt med skrivaren om serialisering.
  • För icke-nullbara värdetyper:

    • Den skickas JsonTokenType.Null till anpassade konverterare vid deserialisering. (Om ingen anpassad konverterare är tillgänglig genereras ett JsonException undantag av den interna konverteraren för typen.)

Det här null-hanteringsbeteendet är främst för att optimera prestanda genom att hoppa över ett extra anrop till konverteraren. Dessutom undviker den att tvinga konverterare för null-typer att söka efter null i början av varje Read och Write metod åsidosättning.

Om du vill aktivera en anpassad konverterare att hantera null för en referens- eller värdetyp åsidosätter du JsonConverter<T>.HandleNull för att returnera true, som du ser i följande exempel:

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.

Bevara referenser

Som standard cachelagras endast referensdata för varje anrop till Serialize eller Deserialize. Om du vill spara referenser från ett Serialize/Deserialize anrop till ett annat rotar du instansen ReferenceResolver på anropsplatsen Serialize/Deserializeför . Följande kod visar ett exempel för det här scenariot:

  • Du skriver en anpassad konverterare för Company typen.
  • Du vill inte serialisera Supervisor egenskapen manuellt, som är en Employee. Du vill delegera det till serialiseraren och du vill också bevara de referenser som du redan har sparat.

Här är klasserna Employee och Company :

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

Konverteraren ser ut så här:

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

En klass som härleds från ReferenceResolver lagrar referenserna i en ordlista:

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

En klass som härleds från ReferenceHandler innehåller en instans av MyReferenceResolver och skapar endast en ny instans när det behövs (i en metod med namnet Reset i det här exemplet):

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

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

När exempelkoden anropar serialiseraren använder den ReferenceHandler en JsonSerializerOptions instans där egenskapen är inställd på en instans av MyReferenceHandler. När du följer det här mönstret måste du återställa ReferenceResolver ordlistan när du är klar med serialiseringen, så att den inte växer för alltid.

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

Föregående exempel utför endast serialisering, men en liknande metod kan användas för deserialisering.

Andra anpassade konverterarexempel

Artikeln Migrera från Newtonsoft.Json till System.Text.Json innehåller ytterligare exempel på anpassade konverterare.

Mappen enhetstest i källkoden System.Text.Json.Serialization innehåller andra anpassade konverterarexempel, till exempel:

Om du behöver göra en konverterare som ändrar beteendet för en befintlig inbyggd konverterare kan du hämta källkoden för den befintliga konverteraren så att den fungerar som en startpunkt för anpassning.

Ytterligare resurser