How to migrate from Newtonsoft.Json to System.Text.Json

This article shows how to migrate from Newtonsoft.Json to System.Text.Json.

The System.Text.Json namespace provides functionality for serializing to and deserializing from JavaScript Object Notation (JSON). The System.Text.Json library is included in the .NET Core 3.0 shared framework. For other target frameworks, install the System.Text.Json NuGet package. The package supports:

  • .NET Standard 2.0 and later versions
  • .NET Framework 4.7.2 and later versions
  • .NET Core 2.0, 2.1, and 2.2

System.Text.Json focuses primarily on performance, security, and standards compliance. It has some key differences in default behavior and doesn't aim to have feature parity with Newtonsoft.Json. For some scenarios, System.Text.Json has no built-in functionality, but there are recommended workarounds. For other scenarios, workarounds are impractical. If your application depends on a missing feature, consider filing an issue to find out if support for your scenario can be added.

Most of this article is about how to use the JsonSerializer API, but it also includes guidance on how to use the JsonDocument (which represents the Document Object Model or DOM), Utf8JsonReader, and Utf8JsonWriter types.

Table of differences between Newtonsoft.Json and System.Text.Json

The following table lists Newtonsoft.Json features and System.Text.Json equivalents. The equivalents fall into the following categories:

  • Supported by built-in functionality. Getting similar behavior from System.Text.Json may require the use of an attribute or global option.
  • Not supported, workaround is possible. The workarounds are custom converters, which may not provide complete parity with Newtonsoft.Json functionality. For some of these, sample code is provided as examples. If you rely on these Newtonsoft.Json features, migration will require modifications to your .NET object models or other code changes.
  • Not supported, workaround is not practical or possible. If you rely on these Newtonsoft.Json features, migration will not be possible without significant changes.
Newtonsoft.Json feature System.Text.Json equivalent
Case-insensitive deserialization by default ✔️ PropertyNameCaseInsensitive global setting
Camel-case property names ✔️ PropertyNamingPolicy global setting
Minimal character escaping ✔️ Strict character escaping, configurable
NullValueHandling.Ignore global setting ✔️ IgnoreNullValues global option
Allow comments ✔️ ReadCommentHandling global setting
Allow trailing commas ✔️ AllowTrailingCommas global setting
Custom converter registration ✔️ Order of precedence differs
No maximum depth by default ✔️ Default maximum depth 64, configurable
Support for a broad range of types ⚠️ Some types require custom converters
Deserialize strings as numbers ⚠️ Not supported, workaround, sample
Deserialize Dictionary with non-string key ⚠️ Not supported, workaround, sample
Polymorphic serialization ⚠️ Not supported, workaround, sample
Polymorphic deserialization ⚠️ Not supported, workaround, sample
Deserialize inferred type to object properties ⚠️ Not supported, workaround, sample
Deserialize JSON null literal to non-nullable value types ⚠️ Not supported, workaround, sample
Deserialize to immutable classes and structs ⚠️ Not supported, workaround, sample
[JsonConstructor] attribute ⚠️ Not supported, workaround, sample
Required setting on [JsonProperty] attribute ⚠️ Not supported, workaround, sample
NullValueHandling setting on [JsonProperty] attribute ⚠️ Not supported, workaround, sample
DefaultValueHandling setting on [JsonProperty] attribute ⚠️ Not supported, workaround, sample
DefaultValueHandling global setting ⚠️ Not supported, workaround, sample
DefaultContractResolver to exclude properties ⚠️ Not supported, workaround, sample
DateTimeZoneHandling, DateFormatString settings ⚠️ Not supported, workaround, sample
Callbacks ⚠️ Not supported, workaround, sample
Support for public and non-public fields ⚠️ Not supported, workaround
Support for internal and private property setters and getters ⚠️ Not supported, workaround
JsonConvert.PopulateObject method ⚠️ Not supported, workaround
ObjectCreationHandling global setting ⚠️ Not supported, workaround
Add to collections without setters ⚠️ Not supported, workaround
PreserveReferencesHandling global setting Not supported
ReferenceLoopHandling global setting Not supported
Support for System.Runtime.Serialization attributes Not supported
MissingMemberHandling global setting Not supported
Allow property names without quotes Not supported
Allow single quotes around string values Not supported
Allow non-string JSON values for string properties Not supported

This is not an exhaustive list of Newtonsoft.Json features. The list includes many of the scenarios that have been requested in GitHub issues or StackOverflow posts. If you implement a workaround for one of the scenarios listed here that doesn't currently have sample code, and if you want to share your solution, select This page in the Feedback section at the bottom of this page. That creates an issue in this documentation's GitHub repo and lists it in the Feedback section on this page too.

Differences in default JsonSerializer behavior compared to Newtonsoft.Json

System.Text.Json is strict by default and avoids any guessing or interpretation on the caller's behalf, emphasizing deterministic behavior. The library is intentionally designed this way for performance and security. Newtonsoft.Json is flexible by default. This fundamental difference in design is behind many of the following specific differences in default behavior.

Case-insensitive deserialization

During deserialization, Newtonsoft.Json does case-insensitive property name matching by default. The System.Text.Json default is case-sensitive, which gives better performance since it's doing an exact match. For information about how to do case-insensitive matching, see Case-insensitive property matching.

If you're using System.Text.Json indirectly by using ASP.NET Core, you don't need to do anything to get behavior like Newtonsoft.Json. ASP.NET Core specifies the settings for camel-casing property names and case-insensitive matching when it uses System.Text.Json.

Minimal character escaping

During serialization, Newtonsoft.Json is relatively permissive about letting characters through without escaping them. That is, it doesn't replace them with \uxxxx where xxxx is the character's code point. Where it does escape them, it does so by emitting a \ before the character (for example, " becomes \"). System.Text.Json escapes more characters by default to provide defense-in-depth protections against cross-site scripting (XSS) or information-disclosure attacks and does so by using the six-character sequence. System.Text.Json escapes all non-ASCII characters by default, so you don't need to do anything if you're using StringEscapeHandling.EscapeNonAscii in Newtonsoft.Json. System.Text.Json also escapes HTML-sensitive characters, by default. For information about how to override the default System.Text.Json behavior, see Customize character encoding.

Comments

During deserialization, Newtonsoft.Json ignores comments in the JSON by default. The System.Text.Json default is to throw exceptions for comments because the RFC 8259 specification doesn't include them. For information about how to allow comments, see Allow comments and trailing commas.

Trailing commas

During deserialization, Newtonsoft.Json ignores trailing commas by default. It also ignores multiple trailing commas (for example, [{"Color":"Red"},{"Color":"Green"},,]). The System.Text.Json default is to throw exceptions for trailing commas because the RFC 8259 specification doesn't allow them. For information about how to make System.Text.Json accept them, see Allow comments and trailing commas. There's no way to allow multiple trailing commas.

Converter registration precedence

The Newtonsoft.Json registration precedence for custom converters is as follows:

  • Attribute on property
  • Attribute on type
  • Converters collection

This order means that a custom converter in the Converters collection is overridden by a converter that is registered by applying an attribute at the type level. Both of those registrations are overridden by an attribute at the property level.

The System.Text.Json registration precedence for custom converters is different:

  • Attribute on property
  • Converters collection
  • Attribute on type

The difference here is that a custom converter in the Converters collection overrides an attribute at the type level. The intention behind this order of precedence is to make run-time changes override design-time choices. There's no way to change the precedence.

For more information about custom converter registration, see Register a custom converter.

Maximum depth

Newtonsoft.Json doesn't have a maximum depth limit by default. For System.Text.Json there's a default limit of 64, and it's configurable by setting JsonSerializerOptions.MaxDepth.

JSON strings (property names and string values)

During deserialization, Newtonsoft.Json accepts property names surrounded by double quotes, single quotes, or without quotes. It accepts string values surrounded by double quotes or single quotes. For example, Newtonsoft.Json accepts the following JSON:

{
  "name1": "value",
  'name2': "value",
  name3: 'value'
}

System.Text.Json only accepts property names and string values in double quotes because that format is required by the RFC 8259 specification and is the only format considered valid JSON.

A value enclosed in single quotes results in a JsonException with the following message:

''' is an invalid start of a value.

Non-string values for string properties

Newtonsoft.Json accepts non-string values, such as a number or the literals true and false, for deserialization to properties of type string. Here's an example of JSON that Newtonsoft.Json successfully deserializes to the following class:

{
  "String1": 1,
  "String2": true,
  "String3": false
}
public class ExampleClass
{
    public string String1 { get; set; }
    public string String2 { get; set; }
    public string String3 { get; set; }
}

System.Text.Json doesn't deserialize non-string values into string properties. A non-string value received for a string field results in a JsonException with the following message:

The JSON value could not be converted to System.String.

Scenarios using JsonSerializer that require workarounds

The following scenarios aren't supported by built-in functionality, but workarounds are possible. The workarounds are custom converters, which may not provide complete parity with Newtonsoft.Json functionality. For some of these, sample code is provided as examples. If you rely on these Newtonsoft.Json features, migration will require modifications to your .NET object models or other code changes.

Types without built-in support

System.Text.Json doesn't provide built-in support for the following types:

Custom converters can be implemented for types that don't have built-in support.

Quoted numbers

Newtonsoft.Json can serialize or deserialize numbers represented by JSON strings (surrounded by quotes). For example, it can accept: {"DegreesCelsius":"23"} instead of {"DegreesCelsius":23}. To enable that behavior in System.Text.Json, implement a custom converter like the following example. The converter handles properties defined as long:

  • It serializes them as JSON strings.
  • It accepts JSON numbers and numbers within quotes while deserializing.
using System;
using System.Buffers;
using System.Buffers.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class LongToStringConverter : JsonConverter<long>
    {
        public override long Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
        {
            if (reader.TokenType == JsonTokenType.String)
            {
                ReadOnlySpan<byte> span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
                if (Utf8Parser.TryParse(span, out long number, out int bytesConsumed) && span.Length == bytesConsumed)
                    return number;

                if (Int64.TryParse(reader.GetString(), out number))
                    return number;
            }

            return reader.GetInt64();
        }

        public override void Write(Utf8JsonWriter writer, long longValue, JsonSerializerOptions options)
        {
            writer.WriteStringValue(longValue.ToString());
        }
    }
}

Register this custom converter by using an attribute on individual long properties or by adding the converter to the Converters collection.

Dictionary with non-string key

Newtonsoft.Json supports collections of type Dictionary<TKey, TValue>. The built-in support for dictionary collections in System.Text.Json is limited to Dictionary<string, TValue>. That is, the key must be a string.

To support a dictionary with an integer or some other type as the key, create a converter like the example in How to write custom converters.

Polymorphic serialization

Newtonsoft.Json automatically does polymorphic serialization. For information about the limited polymorphic serialization capabilities of System.Text.Json, see Serialize properties of derived classes.

The workaround described there is to define properties that may contain derived classes as type object. If that isn't possible, another option is to create a converter with a Write method for the whole inheritance type hierarchy like the example in How to write custom converters.

Polymorphic deserialization

Newtonsoft.Json has a TypeNameHandling setting that adds type name metadata to the JSON while serializing. It uses the metadata while deserializing to do polymorphic deserialization. System.Text.Json can do a limited range of polymorphic serialization but not polymorphic deserialization.

To support polymorphic deserialization, create a converter like the example in How to write custom converters.

Deserialization of object properties

When Newtonsoft.Json deserializes to Object, it:

  • Infers the type of primitive values in the JSON payload (other than null) and returns the stored string, long, double, boolean, or DateTime as a boxed object. Primitive values are single JSON values such as a JSON number, string, true, false, or null.
  • Returns a JObject or JArray for complex values in the JSON payload. Complex values are collections of JSON key-value pairs within braces ({}) or lists of values within brackets ([]). The properties and values within the braces or brackets can have additional properties or values.
  • Returns a null reference when the payload has the null JSON literal.

System.Text.Json stores a boxed JsonElement for both primitive and complex values whenever deserializing to Object, for example:

  • An object property.
  • An object dictionary value.
  • An object array value.
  • A root object.

However, System.Text.Json treats null the same as Newtonsoft.Json and returns a null reference when the payload has the null JSON literal in it.

To implement type inference for object properties, create a converter like the example in How to write custom converters.

Deserialize null to non-nullable type

Newtonsoft.Json doesn't throw an exception in the following scenario:

  • NullValueHandling is set to Ignore, and
  • During deserialization, the JSON contains a null value for a non-nullable value type.

In the same scenario, System.Text.Json does throw an exception. (The corresponding null handling setting is JsonSerializerOptions.IgnoreNullValues.)

If you own the target type, the best workaround is to make the property in question nullable (for example, change int to int?).

Another workaround is to make a converter for the type, such as the following example that handles null values for DateTimeOffset types:

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

namespace SystemTextJsonSamples
{
    public class DateTimeOffsetNullHandlingConverter : JsonConverter<DateTimeOffset>

    {
        public override DateTimeOffset Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options)
        {
            if (reader.TokenType == JsonTokenType.Null)
            {
                return default;
            }
            return reader.GetDateTimeOffset();
        }

        public override void Write(
            Utf8JsonWriter writer,
            DateTimeOffset dateTimeValue,
            JsonSerializerOptions options)
        {
            writer.WriteStringValue(dateTimeValue);
        }
    }
}

Register this custom converter by using an attribute on the property or by adding the converter to the Converters collection.

Note: The preceding converter handles null values differently than Newtonsoft.Json does for POCOs that specify default values. For example, suppose the following code represents your target object:

public class WeatherForecastWithDefault
{
    public WeatherForecastWithDefault()
    {
        Date = DateTimeOffset.Parse("2001-01-01");
        Summary = "No summary";
    }
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string Summary { get; set; }
}

And suppose the following JSON is deserialized by using the preceding converter:

{
  "Date": null,
  "TemperatureCelsius": 25,
  "Summary": null
}

After deserialization, the Date property has 1/1/0001 (default(DateTimeOffset)), that is, the value set in the constructor is overwritten. Given the same POCO and JSON, Newtonsoft.Json deserialization would leave 1/1/2001 in the Date property.

Deserialize to immutable classes and structs

Newtonsoft.Json can deserialize to immutable classes and structs because it can use constructors that have parameters. System.Text.Json supports only public parameterless constructors. As a workaround, you can call a constructor with parameters in a custom converter.

Here's an immutable struct with multiple constructor parameters:

public readonly struct ImmutablePoint
{
    public ImmutablePoint(int x, int y)
    {
        X = x;
        Y = y;
    }

    public int X { get; }
    public int Y { get; }
}

And here's a converter that serializes and deserializes this struct:

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

namespace SystemTextJsonSamples
{
    public class ImmutablePointConverter : JsonConverter<ImmutablePoint>
    {
        private readonly JsonEncodedText XName = JsonEncodedText.Encode("X");
        private readonly JsonEncodedText YName = JsonEncodedText.Encode("Y");

        private readonly JsonConverter<int> _intConverter;

        public ImmutablePointConverter(JsonSerializerOptions options)
        {
            if (options?.GetConverter(typeof(int)) is JsonConverter<int> intConverter)
            {
                _intConverter = intConverter;
            }
            else
            {
                throw new InvalidOperationException();
            }
        }

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

            int x = default;
            bool xSet = false;

            int y = default;
            bool ySet = false;

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

            if (reader.ValueTextEquals(XName.EncodedUtf8Bytes))
            {
                x = ReadProperty(ref reader, options);
                xSet = true;
            }
            else if (reader.ValueTextEquals(YName.EncodedUtf8Bytes))
            {
                y = ReadProperty(ref reader, options);
                ySet = true;
            }
            else
            {
                throw new JsonException();
            }

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

            if (xSet && reader.ValueTextEquals(YName.EncodedUtf8Bytes))
            {
                y = ReadProperty(ref reader, options);
            }
            else if (ySet && reader.ValueTextEquals(XName.EncodedUtf8Bytes))
            {
                x = ReadProperty(ref reader, options);
            }
            else
            {
                throw new JsonException();
            }

            reader.Read();

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

            return new ImmutablePoint(x, y);
        }

        private int ReadProperty(ref Utf8JsonReader reader, JsonSerializerOptions options)
        {
            Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);

            reader.Read();
            return _intConverter.Read(ref reader, typeof(int), options);
        }

        private void WriteProperty(Utf8JsonWriter writer, JsonEncodedText name, int intValue, JsonSerializerOptions options)
        {
            writer.WritePropertyName(name);
            _intConverter.Write(writer, intValue, options);
        }

        public override void Write(
            Utf8JsonWriter writer,
            ImmutablePoint point,
            JsonSerializerOptions options)
        {
            writer.WriteStartObject();
            WriteProperty(writer, XName, point.X, options);
            WriteProperty(writer, YName, point.Y, options);
            writer.WriteEndObject();
        }
    }
}

Register this custom converter by adding the converter to the Converters collection.

For an example of a similar converter that handles open generic properties, see the built-in converter for key-value pairs.

Specify constructor to use

The Newtonsoft.Json [JsonConstructor] attribute lets you specify which constructor to call when deserializing to a POCO. System.Text.Json supports only parameterless constructors. As a workaround, you can call whichever constructor you need in a custom converter. See the example for Deserialize to immutable classes and structs.

Required properties

In Newtonsoft.Json, you specify that a property is required by setting Required on the [JsonProperty] attribute. Newtonsoft.Json throws an exception if no value is received in the JSON for a property marked as required.

System.Text.Json doesn't throw an exception if no value is received for one of the properties of the target type. For example, if you have a WeatherForecast class:

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

The following JSON is deserialized without error:

{
    "TemperatureCelsius": 25,
    "Summary": "Hot"
}

To make deserialization fail if no Date property is in the JSON, implement a custom converter. The following sample converter code throws an exception if the Date property isn't set after deserialization is complete:

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

namespace SystemTextJsonSamples
{
    public class WeatherForecastRequiredPropertyConverter : JsonConverter<WeatherForecast>
    {
        public override WeatherForecast Read(
            ref Utf8JsonReader reader,
            Type type,
            JsonSerializerOptions options)
        {
            // Don't pass in options when recursively calling Deserialize.
            WeatherForecast forecast = JsonSerializer.Deserialize<WeatherForecast>(ref reader);

            // Check for required fields set by values in JSON
            if (forecast.Date == default)
            {
                throw new JsonException("Required property not received in the JSON");
            }
            return forecast;
        }

        public override void Write(
            Utf8JsonWriter writer,
            WeatherForecast forecast, JsonSerializerOptions options)
        {
            // Don't pass in options when recursively calling Serialize.
            JsonSerializer.Serialize(writer, forecast);
        }
    }
}

Register this custom converter by using an attribute on the POCO class or by adding the converter to the Converters collection.

If you follow this pattern, don't pass in the options object when recursively calling Serialize or Deserialize. The options object contains the Converters collection. If you pass it in to Serialize or Deserialize, the custom converter calls into itself, making an infinite loop that results in a stack overflow exception. If the default options are not feasible, create a new instance of the options with the settings that you need. This approach will be slow since each new instance caches independently.

The preceding converter code is a simplified example. Additional logic would be required if you need to handle attributes (such as [JsonIgnore] or different options (such as custom encoders). Also, the example code doesn't handle properties for which a default value is set in the constructor. And this approach doesn't differentiate between the following scenarios:

  • A property is missing from the JSON.
  • A property for a non-nullable type is present in the JSON, but the value is the default for the type, such as zero for an int.
  • A property for a nullable value type is present in the JSON, but the value is null.

Conditionally ignore a property

Newtonsoft.Json has several ways to conditionally ignore a property on serialization or deserialization:

  • DefaultContractResolver lets you select properties to include or exclude, based on arbitrary criteria.
  • The NullValueHandling and DefaultValueHandling settings on JsonSerializerSettings let you specify that all null-value or default-value properties should be ignored.
  • The NullValueHandling and DefaultValueHandling settings on the [JsonProperty] attribute let you specify individual properties that should be ignored when set to null or the default value.

System.Text.Json provides the following ways to omit properties while serializing:

  • The [JsonIgnore] attribute on a property causes the property to be omitted from the JSON during serialization.
  • The IgnoreNullValues global option lets you exclude all null-value properties.
  • The IgnoreReadOnlyProperties global option lets you exclude all read-only properties.

These options don't let you:

  • Ignore all properties that have the default value for the type.
  • Ignore selected properties that have the default value for the type.
  • Ignore selected properties if their value is null.
  • Ignore selected properties based on arbitrary criteria evaluated at run time.

For that functionality, you can write a custom converter. Here's a sample POCO and a custom converter for it that illustrates this approach:

public class WeatherForecast
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string Summary { get; set; }
}
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class WeatherForecastRuntimeIgnoreConverter : JsonConverter<WeatherForecast>
    {
        public override WeatherForecast Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.StartObject)
            {
                throw new JsonException();
            }

            var wf = new WeatherForecast();

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

                if (reader.TokenType == JsonTokenType.PropertyName)
                {
                    string propertyName = reader.GetString();
                    reader.Read();
                    switch (propertyName)
                    {
                        case "Date":
                            DateTimeOffset date = reader.GetDateTimeOffset();
                            wf.Date = date;
                            break;
                        case "TemperatureCelsius":
                            int temperatureCelsius = reader.GetInt32();
                            wf.TemperatureCelsius = temperatureCelsius;
                            break;
                        case "Summary":
                            string summary = reader.GetString();
                            wf.Summary = string.IsNullOrWhiteSpace(summary) ? "N/A" : summary;
                            break;
                    }
                }
            }

            throw new JsonException();
        }

        public override void Write(Utf8JsonWriter writer, WeatherForecast wf, JsonSerializerOptions options)
        {
            writer.WriteStartObject();

            writer.WriteString("Date", wf.Date);
            writer.WriteNumber("TemperatureCelsius", wf.TemperatureCelsius);
            if (!string.IsNullOrWhiteSpace(wf.Summary) && wf.Summary != "N/A")
            {
                writer.WriteString("Summary", wf.Summary);
            }

            writer.WriteEndObject();
        }
    }
}

The converter causes the Summary property to be omitted from serialization if its value is null, an empty string, or "N/A".

Register this custom converter by using an attribute on the class or by adding the converter to the Converters collection.

This approach requires additional logic if:

  • The POCO includes complex properties.
  • You need to handle attributes such as [JsonIgnore] or options such as custom encoders.

Specify date format

Newtonsoft.Json provides several ways to control how properties of DateTime and DateTimeOffset types are serialized and deserialized:

  • The DateTimeZoneHandling setting can be used to serialize all DateTime values as UTC dates.
  • The DateFormatString setting and DateTime converters can be used to customize the format of date strings.

In System.Text.Json, the only format that has built-in support is ISO 8601-1:2019 since it's widely adopted, unambiguous, and makes round trips precisely. To use any other format, create a custom converter. For more information, see DateTime and DateTimeOffset support in System.Text.Json.

Callbacks

Newtonsoft.Json lets you execute custom code at several points in the serialization or deserialization process:

  • OnDeserializing (when beginning to deserialize an object)
  • OnDeserialized (when finished deserializing an object)
  • OnSerializing (when beginning to serialize an object)
  • OnSerialized (when finished serializing an object)

In System.Text.Json, you can simulate callbacks by writing a custom converter. The following example shows a custom converter for a POCO. The converter includes code that displays a message at each point that corresponds to a Newtonsoft.Json callback.

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

namespace SystemTextJsonSamples
{
    public class WeatherForecastCallbacksConverter : JsonConverter<WeatherForecast>
    {
        public override WeatherForecast Read(
            ref Utf8JsonReader reader,
            Type type,
            JsonSerializerOptions options)
        {
            // Place "before" code here (OnDeserializing),
            // but note that there is no access here to the POCO instance.
            Console.WriteLine("OnDeserializing");

            // Don't pass in options when recursively calling Deserialize.
            WeatherForecast forecast = JsonSerializer.Deserialize<WeatherForecast>(ref reader);

            // Place "after" code here (OnDeserialized)
            Console.WriteLine("OnDeserialized");

            return forecast;
        }

        public override void Write(
            Utf8JsonWriter writer,
            WeatherForecast forecast, JsonSerializerOptions options)
        {
            // Place "before" code here (OnSerializing)
            Console.WriteLine("OnSerializing");

            // Don't pass in options when recursively calling Serialize.
            JsonSerializer.Serialize(writer, forecast);

            // Place "after" code here (OnSerialized)
            Console.WriteLine("OnSerialized");
        }
    }
}

Register this custom converter by using an attribute on the class or by adding the converter to the Converters collection.

If you use a custom converter that follows the preceding sample:

  • The OnDeserializing code doesn't have access to the new POCO instance. To manipulate the new POCO instance at the start of deserialization, put that code in the POCO constructor.
  • Don't pass in the options object when recursively calling Serialize or Deserialize. The options object contains the Converters collection. If you pass it in to Serialize or Deserialize, the converter will be used, making an infinite loop that results in a stack overflow exception.

Public and non-public fields

Newtonsoft.Json can serialize and deserialize fields as well as properties. System.Text.Json only works with public properties. Custom converters can provide this functionality.

Internal and private property setters and getters

Newtonsoft.Json can use private and internal property setters and getters via the JsonProperty attribute. System.Text.Json supports only public setters. Custom converters can provide this functionality.

Populate existing objects

The JsonConvert.PopulateObject method in Newtonsoft.Json deserializes a JSON document to an existing instance of a class, instead of creating a new instance. System.Text.Json always creates a new instance of the target type by using the default public parameterless constructor. Custom converters can deserialize to an existing instance.

Reuse rather than replace properties

The Newtonsoft.Json ObjectCreationHandling setting lets you specify that objects in properties should be reused rather than replaced during deserialization. System.Text.Json always replaces objects in properties. Custom converters can provide this functionality.

Add to collections without setters

During deserialization, Newtonsoft.Json adds objects to a collection even if the property has no setter. System.Text.Json ignores properties that don't have setters. Custom converters can provide this functionality.

Scenarios that JsonSerializer currently doesn't support

For the following scenarios, workarounds are not practical or possible. If you rely on these Newtonsoft.Json features, migration will not be possible without significant changes.

Preserve object references and handle loops

By default, Newtonsoft.Json serializes by value. For example, if an object contains two properties that contain a reference to the same Person object, the values of that Person object's properties are duplicated in the JSON.

Newtonsoft.Json has a PreserveReferencesHandling setting on JsonSerializerSettings that lets you serialize by reference:

  • An identifier metadata is added to the JSON created for the first Person object.
  • The JSON that is created for the second Person object contains a reference to that identifier instead of property values.

Newtonsoft.Json also has a ReferenceLoopHandling setting that lets you ignore circular references rather than throw an exception.

System.Text.Json only supports serialization by value and throws an exception for circular references.

System.Runtime.Serialization attributes

System.Text.Json doesn't support attributes from the System.Runtime.Serialization namespace, such as DataMemberAttribute and IgnoreDataMemberAttribute.

Octal numbers

Newtonsoft.Json treats numbers with a leading zero as octal numbers. System.Text.Json doesn't allow leading zeroes because the RFC 8259 specification doesn't allow them.

MissingMemberHandling

Newtonsoft.Json can be configured to throw exceptions during deserialization if the JSON includes properties that are missing in the target type. System.Text.Json ignores extra properties in the JSON, except when you use the [JsonExtensionData] attribute. There's no workaround for the missing member feature.

TraceWriter

Newtonsoft.Json lets you debug by using a TraceWriter to view logs that are generated by serialization or deserialization. System.Text.Json doesn't do logging.

JsonDocument and JsonElement compared to JToken (like JObject, JArray)

System.Text.Json.JsonDocument provides the ability to parse and build a read-only Document Object Model (DOM) from existing JSON payloads. The DOM provides random access to data in a JSON payload. The JSON elements that compose the payload can be accessed via the JsonElement type. The JsonElement type provides APIs to convert JSON text to common .NET types. JsonDocument exposes a RootElement property.

JsonDocument is IDisposable

JsonDocument builds an in-memory view of the data into a pooled buffer. Therefore, unlike JObject or JArray from Newtonsoft.Json, the JsonDocument type implements IDisposable and needs to be used inside a using block.

Only return a JsonDocument from your API if you want to transfer lifetime ownership and dispose responsibility to the caller. In most scenarios, that isn't necessary. If the caller needs to work with the entire JSON document, return the Clone of the RootElement, which is a JsonElement. If the caller needs to work with a particular element within the JSON document, return the Clone of that JsonElement. If you return the RootElement or a sub-element directly without making a Clone, the caller won't be able to access the returned JsonElement after the JsonDocument that owns it is disposed.

Here's an example that requires you to make a Clone:

public JsonElement LookAndLoad(JsonElement source)
{
    string json = File.ReadAllText(source.GetProperty("fileName").GetString());

    using (JsonDocument doc = JsonDocument.Parse(json))
    {
        return doc.RootElement.Clone();
    }
}

The preceding code expects a JsonElement that contains a fileName property. It opens the JSON file and creates a JsonDocument. The method assumes that the caller wants to work with the entire document, so it returns the Clone of the RootElement.

If you receive a JsonElement and are returning a sub-element, it's not necessary to return a Clone of the sub-element. The caller is responsible for keeping alive the JsonDocument that the passed-in JsonElement belongs to. For example:

public JsonElement ReturnFileName(JsonElement source)
{
   return source.GetProperty("fileName");
}

JsonDocument is read-only

The System.Text.Json DOM can't add, remove, or modify JSON elements. It's designed this way for performance and to reduce allocations for parsing common JSON payload sizes (that is, < 1 MB). If your scenario currently uses a modifiable DOM, one of the following workarounds might be feasible:

  • To build a JsonDocument from scratch (that is, without passing in an existing JSON payload to the Parse method), write the JSON text by using the Utf8JsonWriter and parse the output from that to make a new JsonDocument.
  • To modify an existing JsonDocument, use it to write JSON text, making changes while you write, and parse the output from that to make a new JsonDocument.
  • To merge existing JSON documents, equivalent to the JObject.Merge or JContainer.Merge APIs from Newtonsoft.Json, see this GitHub issue.

JsonElement is a union struct

JsonDocument exposes the RootElement as a property of type JsonElement, which is a union, struct type that encompasses any JSON element. Newtonsoft.Json uses dedicated hierarchical types like JObject,JArray, JToken, and so forth. JsonElement is what you can search and enumerate over, and you can use JsonElement to materialize JSON elements into .NET types.

How to search a JsonDocument and JsonElement for sub-elements

Searches for JSON tokens using JObject or JArray from Newtonsoft.Json tend to be relatively fast because they're lookups in some dictionary. By comparison, searches on JsonElement require a sequential search of the properties and hence is relatively slow (for example when using TryGetProperty). System.Text.Json is designed to minimize initial parse time rather than lookup time. Therefore, use the following approaches to optimize performance when searching through a JsonDocument object:

  • Use the built-in enumerators (EnumerateArray and EnumerateObject) rather than doing your own indexing or loops.
  • Don't do a sequential search on the whole JsonDocument through every property by using RootElement. Instead, search on nested JSON objects based on the known structure of the JSON data. For example, if you're looking for a Grade property in Student objects, loop through the Student objects and get the value of Grade for each, rather than searching through all JsonElement objects looking for Grade properties. Doing the latter will result in unnecessary passes over the same data.

For a code example, see Use JsonDocument for access to data.

Utf8JsonReader compared to JsonTextReader

System.Text.Json.Utf8JsonReader is a high-performance, low allocation, forward-only reader for UTF-8 encoded JSON text, read from a ReadOnlySpan<byte> or ReadOnlySequence<byte>. The Utf8JsonReader is a low-level type that can be used to build custom parsers and deserializers.

The following sections explain recommended programming patterns for using Utf8JsonReader.

Utf8JsonReader is a ref struct

Because the Utf8JsonReader type is a ref struct, it has certain limitations. For example, it can't be stored as a field on a class or struct other than a ref struct. To achieve high performance, this type must be a ref struct since it needs to cache the input ReadOnlySpan<byte>, which itself is a ref struct. In addition, this type is mutable since it holds state. Therefore, pass it by ref rather than by value. Passing it by value would result in a struct copy and the state changes would not be visible to the caller. This differs from Newtonsoft.Json since the Newtonsoft.Json JsonTextReader is a class. For more information about how to use ref structs, see Write safe and efficient C# code.

Read UTF-8 text

To achieve the best possible performance while using the Utf8JsonReader, read JSON payloads already encoded as UTF-8 text rather than as UTF-16 strings. For a code example, see Filter data using Utf8JsonReader.

Read with a Stream or PipeReader

The Utf8JsonReader supports reading from a UTF-8 encoded ReadOnlySpan<byte> or ReadOnlySequence<byte> (which is the result of reading from a PipeReader).

For synchronous reading, you could read the JSON payload until the end of the stream into a byte array and pass that into the reader. For reading from a string (which is encoded as UTF-16), call UTF8.GetBytes to first transcode the string to a UTF-8 encoded byte array. Then pass that to the Utf8JsonReader.

Since the Utf8JsonReader considers the input to be JSON text, a UTF-8 byte order mark (BOM) is considered invalid JSON. The caller needs to filter that out before passing the data to the reader.

For code examples, see Use Utf8JsonReader.

Read with multi-segment ReadOnlySequence

If your JSON input is a ReadOnlySpan<byte>, each JSON element can be accessed from the ValueSpan property on the reader as you go through the read loop. However, if your input is a ReadOnlySequence<byte> (which is the result of reading from a PipeReader), some JSON elements might straddle multiple segments of the ReadOnlySequence<byte> object. These elements would not be accessible from ValueSpan in a contiguous memory block. Instead, whenever you have a multi-segment ReadOnlySequence<byte> as input, poll the HasValueSequence property on the reader to figure out how to access the current JSON element. Here's a recommended pattern:

while (reader.Read())
{
    switch (reader.TokenType)
    {
        // ...
        ReadOnlySpan<byte> jsonElement = reader.HasValueSequence ?
            reader.ValueSequence.ToArray() :
            reader.ValueSpan;
        // ...
    }
}

Use ValueTextEquals for property name lookups

Don't use ValueSpan to do byte-by-byte comparisons by calling SequenceEqual for property name lookups. Call ValueTextEquals instead, because that method unescapes any characters that are escaped in the JSON. Here's an example that shows how to search for a property that is named "name":

private static readonly byte[] s_nameUtf8 = Encoding.UTF8.GetBytes("name");
while (reader.Read())
{
    JsonTokenType tokenType = reader.TokenType;

    switch (tokenType)
    {
        case JsonTokenType.StartObject:
            total++;
            break;
        case JsonTokenType.PropertyName:
            if (reader.ValueTextEquals(s_nameUtf8))
            {
                count++;
            }
            break;
    }

Read null values into nullable value types

Newtonsoft.Json provides APIs that return Nullable<T>, such as ReadAsBoolean, which handles a Null TokenType for you by returning a bool?. The built-in System.Text.Json APIs return only non-nullable value types. For example, Utf8JsonReader.GetBoolean returns a bool. It throws an exception if it finds Null in the JSON. The following examples show two ways to handle nulls, one by returning a nullable value type and one by returning the default value:

public bool? ReadAsNullableBoolean()
{
    _reader.Read();
    if (_reader.TokenType == JsonTokenType.Null)
    {
        return null;
    }
    if (_reader.TokenType != JsonTokenType.True && _reader.TokenType != JsonTokenType.False)
    {
        throw new JsonException();
    }
    return _reader.GetBoolean();
}
public bool ReadAsBoolean(bool defaultValue)
{
    _reader.Read();
    if (_reader.TokenType == JsonTokenType.Null)
    {
        return defaultValue;
    }
    if (_reader.TokenType != JsonTokenType.True && _reader.TokenType != JsonTokenType.False)
    {
        throw new JsonException();
    }
    return _reader.GetBoolean();
}

Multi-targeting

If you need to continue to use Newtonsoft.Json for certain target frameworks, you can multi-target and have two implementations. However, this is not trivial and would require some #ifdefs and source duplication. One way to share as much code as possible is to create a ref struct wrapper around Utf8JsonReader and Newtonsoft.Json JsonTextReader. This wrapper would unify the public surface area while isolating the behavioral differences. This lets you isolate the changes mainly to the construction of the type, along with passing the new type around by reference. This is the pattern that the Microsoft.Extensions.DependencyModel library follows:

Utf8JsonWriter compared to JsonTextWriter

System.Text.Json.Utf8JsonWriter is a high-performance way to write UTF-8 encoded JSON text from common .NET types like String, Int32, and DateTime. The writer is a low-level type that can be used to build custom serializers.

The following sections explain recommended programming patterns for using Utf8JsonWriter.

Write with UTF-8 text

To achieve the best possible performance while using the Utf8JsonWriter, write JSON payloads already encoded as UTF-8 text rather than as UTF-16 strings. Use JsonEncodedText to cache and pre-encode known string property names and values as statics, and pass those to the writer, rather than using UTF-16 string literals. This is faster than caching and using UTF-8 byte arrays.

This approach also works if you need to do custom escaping. System.Text.Json doesn't let you disable escaping while writing a string. However, you could pass in your own custom JavaScriptEncoder as an option to the writer, or create your own JsonEncodedText that uses your JavascriptEncoder to do the escaping, and then write the JsonEncodedText instead of the string. For more information, see Customize character encoding.

Write raw values

The Newtonsoft.Json WriteRawValue method writes raw JSON where a value is expected. System.Text.Json has no direct equivalent, but here's a workaround that ensures only valid JSON is written:

using JsonDocument doc = JsonDocument.Parse(string);
doc.WriteTo(writer);

Customize character escaping

The StringEscapeHandling setting of JsonTextWriter offers options to escape all non-ASCII characters or HTML characters. By default, Utf8JsonWriter escapes all non-ASCII and HTML characters. This escaping is done for defense-in-depth security reasons. To specify a different escaping policy, create a JavaScriptEncoder and set JsonWriterOptions.Encoder. For more information, see Customize character encoding.

Customize JSON format

JsonTextWriter includes the following settings, for which Utf8JsonWriter has no equivalent:

  • Indentation - Specifies how many characters to indent. Utf8JsonWriter always does 2-character indentation.
  • IndentChar - Specifies the character to use for indentation. Utf8JsonWriter always uses whitespace.
  • QuoteChar - Specifies the character to use to surround string values. Utf8JsonWriter always uses double quotes.
  • QuoteName - Specifies whether or not to surround property names with quotes. Utf8JsonWriter always surrounds them with quotes.

There are no workarounds that would let you customize the JSON produced by Utf8JsonWriter in these ways.

Write null values

To write null values by using Utf8JsonWriter, call:

  • WriteNull to write a key-value pair with null as the value.
  • WriteNullValue to write null as an element of a JSON array.

For a string property, if the string is null, WriteString and WriteStringValue are equivalent to WriteNull and WriteNullValue.

Write Timespan, Uri, or char values

JsonTextWriter provides WriteValue methods for TimeSpan, Uri, and char values. Utf8JsonWriter doesn't have equivalent methods. Instead, format these values as strings (by calling ToString(), for example) and call WriteStringValue.

Multi-targeting

If you need to continue to use Newtonsoft.Json for certain target frameworks, you can multi-target and have two implementations. However, this is not trivial and would require some #ifdefs and source duplication. One way to share as much code as possible is to create a wrapper around Utf8JsonWriter and Newtonsoft JsonTextWriter. This wrapper would unify the public surface area while isolating the behavioral differences. This lets you isolate the changes mainly to the construction of the type. Microsoft.Extensions.DependencyModel library follows:

Additional resources