Code Generation

System.Text.Json vs Newtonsoft.Json: Performance, Features & Migration Guide

A technical deep-dive comparing System.Text.Json and Newtonsoft.Json (Json.NET) in modern .NET. Analyze UTF-8 pipeline architecture, feature gaps, and migration strategies.

For more than a decade, Newtonsoft.Json (popularly known as Json.NET) was the undisputed foundation of JSON processing across the entire .NET ecosystem. Created by James Newton-King in 2006, it became so essential that Microsoft bundled it directly into ASP.NET Core 2.x and earlier .NET Framework templates.

Starting with .NET Core 3.0 and continuously refined through .NET 6, 7, 8, and 9, Microsoft introduced System.Text.Json (STJ) as a high-performance, low-allocation runtime replacement built from the ground up on modern .NET primitives like Span<T>, ReadOnlySequence<T>, and direct UTF-8 byte streaming.

While System.Text.Json is now the default serializer in ASP.NET Core, transitioning existing codebases or choosing a serializer for high-throughput microservices involves subtle behavioral, functional, and performance trade-offs. This guide breaks down the architectural differences, analyzes feature parity, and provides an actionable checklist for migration.


1. Architectural Differences & Performance Mechanics

The performance gap between System.Text.Json and Newtonsoft.Json stems from fundamental design differences in memory management and text encoding.

The String Allocation Bottleneck in Newtonsoft.Json

Newtonsoft.Json was designed when .NET relied on string-based pipelines. When parsing an incoming HTTP JSON payload, typical processing in Json.NET involves:

  1. Decoding the raw UTF-8 byte stream from the network socket into a managed .NET UTF-16 System.String.
  2. Parsing that string character by character into intermediate AST structures (such as JObject, JToken, or boxed reflection dictionaries).
  3. Instantiating target C# objects, generating significant Gen 0 and Gen 1 Garbage Collection (GC) pressure under heavy request volume.

Zero-Allocation Pipelines in System.Text.Json

System.Text.Json bypasses intermediate UTF-16 string conversion entirely:

  • Utf8JsonReader: A high-speed, non-allocating, forward-only tokenizer that reads directly from ReadOnlySpan<byte>.
  • Utf8JsonWriter: Writes directly to byte buffers without intermediate string formatting.
  • Source Generators (.NET 6+): By adding [JsonSerializable] contexts, the C# compiler generates serialization logic at compile time, eliminating runtime reflection entirely. This is essential for Native AOT (Ahead-of-Time) compilation.

In standard HTTP API benchmarks, System.Text.Json typically delivers 2x to 3x higher throughput and reduces memory allocations by 70% to 90% compared to default Newtonsoft.Json configurations.

If you are generating C# DTO models from API responses to benchmark both serializers, you can use our online JSON to C# generator to toggle between System.Text.Json [JsonPropertyName] and Newtonsoft [JsonProperty] attributes instantly.


2. Feature Parity & How to Bridge the Gaps

While System.Text.Json is substantially faster, Newtonsoft.Json remains more feature-rich and forgiving of malformed or irregular payloads. Below is an overview of major feature gaps and their modern .NET workarounds.

Feature AreaNewtonsoft.Json (Json.NET)System.Text.Json (.NET 8+)Migration Workaround / Solution
Case InsensitivityCase-insensitive by defaultStrictly case-sensitive by defaultSet PropertyNameCaseInsensitive = true in JsonSerializerOptions.
Polymorphic TypesTypeNameHandling.Auto / All[JsonDerivedType] attribute (.NET 7+)Use explicit discriminator attributes or implement a custom JsonConverter<T>.
Circular ReferencesReferenceLoopHandling.IgnoreReferenceHandler.IgnoreCyclesSet options.ReferenceHandler = ReferenceHandler.IgnoreCycles.
Dynamic JSON TreeJObject, JArray, JTokenJsonNode, JsonObject, JsonElementJsonDocument/JsonElement (read-only) or JsonNode (.NET 6+ mutable DOM).
Quoted NumbersAutomatically coerces "123" to intFails with JsonException by defaultSet NumberHandling = JsonNumberHandling.AllowReadingFromString.
Comments & Trailing CommasSupported by defaultFails with JsonException by defaultSet ReadCommentHandling = JsonCommentHandling.Skip and AllowTrailingCommas = true.

Handling Polymorphism

In Newtonsoft.Json, serializing derived types was often handled via TypeNameHandling:

// Newtonsoft.Json (Legacy approach - potential security vulnerability if unconstrained)
var settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto };

In modern System.Text.Json (.NET 7 and .NET 8+), polymorphism is type-safe and declared explicitly via type discriminators:

using System.Text.Json.Serialization;

[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(CreditCardPayment), "credit_card")]
[JsonDerivedType(typeof(BankTransferPayment), "bank_transfer")]
public abstract class PaymentMethod
{
    public decimal Amount { get; set; }
}

public class CreditCardPayment : PaymentMethod
{
    public string CardNumber { get; set; } = string.Empty;
}

public class BankTransferPayment : PaymentMethod
{
    public string Iban { get; set; } = string.Empty;
}

When dealing with arbitrary JSON payloads without defined C# classes, Newtonsoft developers frequently use JObject.Parse().

System.Text.Json provides two alternatives:

  1. JsonDocument / JsonElement: Lightweight, read-only view backed by memory spans. Ideal for high-performance inspection without mutation.
  2. JsonNode / JsonObject (.NET 6+): A fully mutable DOM comparable to JObject.
using System.Text.Json.Nodes;

// Parsing arbitrary JSON dynamically with System.Text.Json
var node = JsonNode.Parse(jsonString);
string? city = node?["address"]?["city"]?.GetValue<string>();

// Mutating dynamic properties
if (node is JsonObject obj)
{
    obj["status"] = "processed";
    obj.Remove("internalDebugToken");
}

3. Critical Default Behavior Differences

Migrating from Newtonsoft to System.Text.Json often introduces unexpected runtime errors because STJ enforces strict standards compliance by default.

1. Property Name Casing

Newtonsoft matches JSON keys like firstName or first_name to a C# property FirstName flexibly. System.Text.Json requires an exact match unless configured:

var options = new JsonSerializerOptions
{
    // Match camelCase or PascalCase JSON keys to C# properties automatically
    PropertyNameCaseInsensitive = true,
    // Output camelCase JSON when serializing
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

2. Numbers as Strings

Many third-party payment gateways and webhooks return numbers wrapped in quotes ("amount": "49.99"). While Newtonsoft parsed this silently into a decimal or double, System.Text.Json throws a JsonException: The JSON value could not be converted.

var options = new JsonSerializerOptions
{
    NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString
};

3. Escaping Characters (HTML & Unicode)

By default, System.Text.Json escapes characters like <, >, &, and + into \u003C, \u003E, \u0026 to prevent Cross-Site Scripting (XSS) when JSON is embedded directly into HTML script tags. If your application requires raw characters (for example, generating SQL or file system payloads), configure the JavaScript encoder:

using System.Text.Encodings.Web;

var options = new JsonSerializerOptions
{
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};

4. Step-by-Step Migration Checklist

When transitioning an existing production service from Newtonsoft.Json to System.Text.Json, follow this structured verification checklist:

  1. Replace Attribute References:
    • Replace [JsonProperty("key")] with [JsonPropertyName("key")].
    • Replace [JsonIgnore] (Newtonsoft.Json) with [JsonIgnore] (System.Text.Json.Serialization). Note that both share the same name but reside in different namespaces.
  2. Audit Custom Converters:
    • Custom JsonConverter implementations in Newtonsoft derive from Newtonsoft.Json.JsonConverter.
    • In STJ, rewrite them by subclassing JsonConverter<T> and overriding Read() and Write().
  3. Configure ASP.NET Core Controller Defaults: In Program.cs, ensure your global MVC/Minimal API serializer options match your required tolerance:
    builder.Services.ConfigureHttpJsonOptions(options =>
    {
        options.SerializerOptions.PropertyNameCaseInsensitive = true;
        options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
        options.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
    });
  4. Verify Positional Records: If using C# 9+ positional records (public record User(string Name, int Age);), ensure that attributes are prefixed with the property target specifier ([property: JsonPropertyName("name")]) so the serializer attaches the mapping to the generated property.
  5. Execute Contract Integration Tests: Run integration tests against realistic external API payloads containing unexpected fields, missing optional properties, and UTC timestamps to ensure no JsonException is thrown in production.

5. Practical Decision Matrix: When to Choose Which

                            ┌─────────────────────────────────────────┐
                            │ Is your application running on .NET 8+? │
                            └────────────────────┬────────────────────┘

                                ┌────────────────┴────────────────┐
                                │ YES                             │ NO (.NET Framework 4.8)
                                ▼                                 ▼
                 ┌───────────────────────────────┐     ┌──────────────────────┐
                 │ Do you require legacy JObject │     │ Use Newtonsoft.Json  │
                 │ manipulation or BSON formats? │     └──────────────────────┘
                 └──────────────┬────────────────┘

               ┌────────────────┴────────────────┐
               │ NO                              │ YES
               ▼                                 ▼
┌───────────────────────────────┐     ┌──────────────────────────────┐
│ Choose System.Text.Json       │     │ Keep Newtonsoft.Json or use  │
│ (Default for modern .NET APIs)│     │ Microsoft.AspNetCore.Mvc     │
└───────────────────────────────┘     │ .NewtonsoftJson bridge       │
                                      └──────────────────────────────┘
  • Choose System.Text.Json if: You are building high-throughput web APIs, gRPC/REST microservices, cloud-native Azure Functions, or targeting Native AOT compilation where runtime reflection must be eliminated.
  • Stay with Newtonsoft.Json if: You maintain an existing enterprise application on .NET Framework 4.8, rely heavily on JSON Schema validation via Newtonsoft.Json.Schema, or use advanced JSONPath querying through deep JObject hierarchies that would be costly to refactor.

To quickly convert sample JSON payloads into clean C# classes or modern C# 9+ records configured for either serializer, test your structures in the JSON to C# POCO generator.

Try the tools mentioned in this article