Newtonsoft.Json vs System.Text.Json

Just what we needed, choices.

Newtonsoft.Json vs System.Text.Json

posted in dotnet on  •   • 

JamesNK/Newtonsoft.Json

System.Text.Json (STJ) is about twice as fast as Newtonsoft.Json while only consuming 50% as much memory. A legacy project might still be using Newtonsoft but chances are serialization isn’t a bottleneck so it’s probably not worth making the switch.

For new applications you’ll want to stick with STJ however. Only add the extra nuget if you really need one of the advanced Newtonsoft features (ex: Linq to Json, polymorphic serialization, circular reference handling).

System.Text.Json is included in the runtime for .NET Core 3.1 and later. Otherwise use the nuget package.

TL&DR

  • System.Text.Json: The default, fast, memory efficient, but basic features
  • Newtonsoft.Json: Easy, tolerant, lots of features, but much slower

Basic Usage

(De)Serialization with optional settings/options:

// System.Text.Json
var options = new JsonSerializerOptions();
string json = JsonSerializer.Serialize(obj[, options]);
MyClass? obj = JsonSerializer.Deserialize<MyClass>(json[, options]);

// Newtonsoft.Json
var settings = new JsonSerializerSettings();
string json = JsonConvert.SerializeObject(obj[, settings]);
MyClass? obj = JsonConvert.DeserializeObject<MyClass>(json[, settings]);

Configuration

  System.Text.Json Newtonsoft.Json Remarks
Type JsonSerializerOptions JsonSerializerSettings  
  DefaultIgnoreCondition separate properties  
Default JsonIgnoreCondition.WhenWritingDefault DefaultValueHandling Skip default(T)
Null JsonIgnoreCondition.WhenWritingNull NullValueHandling Skip null only
Pretty Print WriteIndented, IndentCharacter, IndentSize, NewLine Formatting  
Numbers NumberHandling FloatFormatHandling See Floats
Dates custom converters DateFormatString, DateFormatHandling, DateTimeZoneHandling, DateParseHandling  
Circular Refs ReferenceHandler ReferenceLoopHandling  
Unexisting properties UnmappedMemberHandling MissingMemberHandling  
Constructor RespectRequiredConstructorParameters ConstructorHandling  

Floats

Serialize float/double NaN, Infinity and -Infinity as strings.

// System.Text.Json
var opts = new JsonSerializerOptions()
{
  NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
  // JsonNumberHandling.Strict (default): Throw ArgumentException for NaN etc
};
var obj = new Floats() { Float = 3.1415f, Float2 = float.NaN };
string json = JsonSerializer.Serialize(obj, opts);
Assert.Equal("{\"Float\":3.1415,\"Float2\":\"NaN\"}", json);

// Serialize all values as strings
opts = new JsonSerializerOptions()
{
  NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString
};
obj = new Floats() { Float = 3.1415f, Float2 = float.PositiveInfinity};
json = JsonSerializer.Serialize(obj, opts);
Assert.Equal("{\"Float\":\"3.1415\",\"Float2\":\"Infinity\"}", json);


// Newtonsoft.Json
var sets = new JsonSerializerSettings()
{
  // "String" is the default behavior:
  FloatFormatHandling = FloatFormatHandling.String,
  // FloatFormatHandling.DefaultValue: Serialize NaN etc as 0.0
  // FloatFormatHandling.Symbol: Serialize NaN etc WITHOUT quotes (=not valid json)
};
var obj = new Floats() { Float = 3.1415f, Float2 = float.NegativeInfinity };
string json = JsonConvert.SerializeObject(obj, sets);
Assert.Equal("{\"Float\":3.1415,\"Float2\":\"-Infinity\"}", json);

// Newtonsoft can also serialize ALL floats as a string with a custom converter
var settings = new JsonSerializerSettings
{
  Converters = new List<JsonConverter> { new StringFloatConverter() },
};

public class StringFloatConverter : JsonConverter
{
  public override bool CanConvert(Type objectType) => objectType == typeof(float);

  public override object ReadJson(JsonReader reader, Type objType, object value, JsonSerializer serializer)
  {
    if (reader.TokenType == JsonToken.String)
      return float.Parse((string)reader.Value);

    return Convert.ToDouble(reader.Value);
  }

  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
  {
    writer.WriteValue(((float)value).ToString(CultureInfo.InvariantCulture));
  }
}

Default Options

Only for Newtonsoft:

JsonConvert.DefaultSettings = () => new JsonSerializerSettings {};

// Read-only for STJ:
var defaultOpts = JsonSerializerOptions.Default;

Performance

Case Sensitivity

One of the reasons System.Text.Json is so much more performant is because Newtonsoft json key names are completely case insensitive, while property name cases have to match exactly in STJ.

public record Person(string Name);

// System.Text.Json
string json = "{\"name\":\"Bert\"}";
var obj = JsonSerializer.Deserialize<Person>(json);
Assert.Null(obj.Name);

// Newtonsoft.Json
string json = "{\"name\":\"Bert\"}";
var obj = JsonConvert.DeserializeObject<Person>(json);
Assert.Equal("Bert", obj.Name);

Postel’s law

Be conservative in what you send, be liberal in what you accept.

Newtonsoft.Json is very liberal in what it will deserialize for you without complaint.

string json = """
{
  // Newtonsoft parses this
  Name: 'Bert',
}
""";

While System.Text.Json throws a JsonException on:

  • The comment
  • The missing quotes around Name
  • The use of single quotes around Bert
  • The extra comma after ‘Bert’

Some of these can still be parsed by STJ with some extra configuration:

var opts = new JsonSerializerOptions()
{
  PropertyNameCaseInsensitive = true,
  ReadCommentHandling = JsonCommentHandling.Skip,
  AllowTrailingCommas = true,
};

Span(Of T)

The main reason for the performance difference however is the use of Span<T> in System.Text.Json. Which is not being adopted by Newtonsoft because of .NET Framework support.

Performance is about 2x but this increases to about 3x for serialization when using the STJ source generation feature to eliminate the use of Reflection.

WebApi

System.Text.Json is the default from .NET Core 3.1 and up.

I’ve added the same “sensible” configuration for both:

  • Use camelCase for property names – which is typically used in JavaScript
  • During development, pretty print the json
  • Do not include null and primitive default values (ex: false, 0) in the output so the bytes sent over the network are somewhat reduced
  • Serialize enums as strings. While this increases the bytes sent, it does make them more human-readable.
var builder = WebApplication.CreateBuilder(args);

// System.Text.Json
builder.Services.AddControllers().AddJsonOptions(options =>
{
  options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;

  if (builder.Environment.IsDevelopment())
    options.JsonSerializerOptions.WriteIndented = true;

  options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault;
  options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});

// System.Text.Json for Minimal APIs (.NET Core 7)
builder.Services.ConfigureHttpJsonOptions(options => { ... });
// If not specified, defaults to:
var opts = new JsonSerializerOptions(JsonSerializerDefaults.Web);
Assert.True(opts.PropertyNameCaseInsensitive);
Assert.Equal(JsonNamingPolicy.CamelCase, opts.PropertyNamingPolicy);
Assert.Equal(JsonNumberHandling.AllowReadingFromString, opts.NumberHandling);


// Newtonsoft.Json
Install-Package Microsoft.AspNetCore.Mvc.NewtonsoftJson

using Newtonsoft.Json.Serialization;
builder.Services.AddControllers().AddNewtonsoftJson(options =>
{
  options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();

  if (builder.Environment.IsDevelopment())
    options.SerializerSettings.Formatting = Formatting.Indented;

  options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
  options.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Ignore;
  options.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
});

Custom Converters

When the serialization options/settings can’t handle your case, maybe because you cannot control how the json communication happens or because some values should be converted to a domain specific type, you can add your own converters.

Imagine that our frontend sends and/or accepts Money in a specific culture, like 15.000,99 (nl-BE formatting).

STJ CulturalMoney

For illustration purposes only; you’ll probably want to handle all sorts of edge cases 😉

new JsonSerializerOptions().Converters.Add(new CulturalMoneyConverter(new CultureInfo("nl-BE")));

public class CulturalMoneyConverter(CultureInfo culture) : JsonConverter<decimal>
{
  public override decimal Read(ref Utf8JsonReader reader, Type toConvert, JsonSerializerOptions opts)
  {
    return reader.TokenType == JsonTokenType.String
      ? decimal.Parse(reader.GetString(), NumberStyles.Any, culture)
      : reader.GetDecimal();
  }

  public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
  {
    writer.WriteStringValue(value.ToString("#,###.##", culture));
  }
}

Newtonsoft CulturalMoney

new JsonSerializerSettings().Converters.Add(new CulturalMoneyConverter(new CultureInfo("nl-BE")));

public class CulturalMoneyConverter(CultureInfo culture) : Newtonsoft.Json.JsonConverter
{
  public override bool CanConvert(Type objectType) => objectType == typeof(decimal);

  public override object ReadJson(JsonReader reader, Type objType, object? value, JsonSerializer js)
  {
    return reader.TokenType == JsonToken.String
      ? decimal.Parse((string)reader.Value, NumberStyles.Any, culture)
      : Convert.ToDecimal(reader.Value);
  }

  public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
  {
    writer.WriteValue(((decimal)value).ToString("#,###.##", culture));
  }
}

Contract Resolvers

If converters aren’t doing it for you, because your frontend team is sending some really weird stuff, it’s still possible to further alter the behavior of serialization.

In STJ this is done by setting the JsonSerializerOptions.TypeInfoResolver, where you could use new DefaultJsonTypeInfoResolver() and set the Modifiers property or provide custom implementation(s) with JsonTypeInfoResolver.Combine(...).

For Newtonsoft, set the JsonSerializerSettings.ContractResolver and inherit from the DefaultContractResolver.

Adoption

Adoption for both libraries is going up, with no decline in sight for Newtonsoft.Json but I’m guessing this is comparing apples and oranges because STJ is included by default and is thus under-represented in the graph.

Newtonsoft.Json vs System.Text.Json nuget downloads graph for the last 10 years

More!!

Polymorphism

Works pretty much out of the box for Newtonsoft. For STJ it is possible to achieve starting from .NET Core 7 with the JsonDerivedTypeAttribute on the base class.

Attributes

Both have attributes that can be applied to fields and/or properties to change serialization behavior without reverting to custom code.

STJ Attributes

public class Person
{
  [JsonPropertyName("person_name")]
  [JsonPropertyOrder(10)]
  public string Name { get; }

  [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
  public int? Age { get; }

  [JsonIgnore]
  public string Password { get; set; }

  // We cannot pass a format to the Converter (like Newtonsoft allows)
  [JsonConverter(typeof(BirthDateFormatConverter))]
  public DateTime BirthDate { get; }

  [JsonConstructor]
  public PersonWithAttributes(string name, int? age = null)
  {
    Name = name; // "person_name" does NOT work
    Age = age;
  }
}

Newtonsoft Attributes

[JsonObject(MemberSerialization.OptOut)]
public class PersonWithAttributes
{
  [JsonProperty("person_name", Order = 10)]
  public string Name { get; }

  [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
  public int? Age { get; }

  [JsonIgnore]
  public string Password { get; set; }

  // Pass the format to the Converter
  [JsonConverter(typeof(DateFormatConverter), "yyyy-MM-dd")]
  public DateTime BirthDate { get; }

  [JsonConstructor]
  public PersonWithAttributes(string name, int? age)
  {
      Name = name; // This parameter could also be called person_name
      Age = age;
  }
}

Callbacks

Execute code before/after (de)serialization:

  • STJ: implement interfaces (IJsonOnSerializing, IJsonOnSerialized, IJsonOnDeserializing, IJsonOnDeserialized)
  • Newtonsoft: attributes on methods (OnSerializing, OnSerialized, OnDeserializing, OnDeserialized)

Stuff that came into being during the making of this post
Other interesting reads
Tags: tutorial