Migrating from Newtonsoft.Json to System.Text.Json
When Microsoft introduced System.Text.Json in .NET Core 3.0, it offered a high-performance, low-allocation alternative to the long-reigning Newtonsoft.Json. Six years on, many codebases still carry the Newtonsoft dependency — sometimes deliberately, sometimes through inertia. If you're considering the switch, here's what you need to know.
Why migrate?
System.Text.Json ships in-box with the .NET runtime. It's faster, allocates less memory, and is the default serializer for ASP.NET Core, Minimal APIs, and the configuration system. Staying on Newtonsoft means pulling in a third-party package and potentially double-serializing in certain ASP.NET Core pipelines.
That said, Newtonsoft remains more feature-rich in edge cases — polymorphic serialization was only fully addressed in System.Text.Json from .NET 7 onwards, and some advanced JsonConverter patterns are still simpler with Newtonsoft.
Key differences at a glance
| Behaviour | Newtonsoft.Json | System.Text.Json |
|---|---|---|
| Default naming | PascalCase | camelCase (in ASP.NET Core) |
| Property matching | Case-insensitive | Case-sensitive by default |
| Comments in JSON | Allowed | Disallowed by default |
| Trailing commas | Allowed | Disallowed by default |
$type discriminator |
Built-in | [JsonPolymorphic] from .NET 7 |
| Missing members | Ignored | Ignored (configurable in .NET 9) |
An incremental migration strategy
Rather than a big-bang rewrite, migrate one serialization boundary at a time. Start with DTOs that have simple shapes — flat objects, primitive types, and collections.
Step 1: Configure compatible options
Create a shared JsonSerializerOptions instance that mirrors Newtonsoft defaults:
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
// IMPORTANT
Always reuse your JsonSerializerOptions instance. Creating one per call is expensive because the first use triggers reflection and caching.
Step 2: Replace serialization calls
Swap out JsonConvert.SerializeObject and JsonConvert.DeserializeObject for their System.Text.Json equivalents:
// Before (Newtonsoft)
var json = JsonConvert.SerializeObject(order);
var order = JsonConvert.DeserializeObject<Order>(json);
// After (System.Text.Json)
var json = JsonSerializer.Serialize(order, options);
var order = JsonSerializer.Deserialize<Order>(json, options);
Step 3: Migrate custom converters
Newtonsoft's JsonConverter<T> maps to System.Text.Json.Serialization.JsonConverter<T>, but the API surface differs. Here's a date-only converter as an example:
public class DateOnlyJsonConverter : JsonConverter<DateOnly>
{
private const string Format = "yyyy-MM-dd";
public override DateOnly Read(
ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return DateOnly.ParseExact(reader.GetString()!, Format, CultureInfo.InvariantCulture);
}
public override void Write(
Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(Format, CultureInfo.InvariantCulture));
}
}
Register it on your options:
options.Converters.Add(new DateOnlyJsonConverter());
Step 4: Handle polymorphic types
If you rely on Newtonsoft's TypeNameHandling for polymorphic serialization, switch to the .NET 7+ [JsonPolymorphic] attribute:
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(CreditCardPayment), "credit-card")]
[JsonDerivedType(typeof(BankTransferPayment), "bank-transfer")]
public abstract class Payment
{
public decimal Amount { get; set; }
}
This is considerably safer than Newtonsoft's TypeNameHandling.All, which has well-documented deserialization vulnerabilities.
What to watch out for
Enums are serialized as numbers by default. Add JsonStringEnumConverter to your options if you want string representation:
options.Converters.Add(new JsonStringEnumConverter());
Private setters are not populated by default. Use [JsonInclude] on properties with private setters that need deserializing.
Dictionary key types must be strings or implement IParsable<T> (from .NET 7). Newtonsoft happily serializes Dictionary<int, string> with integer keys; System.Text.Json will throw unless you provide a converter.
JObject and JToken have no direct equivalent. Use JsonDocument for read-only access or JsonNode (from .NET 6) for mutable DOM manipulation.
When to stay on Newtonsoft
If your codebase heavily uses JObject manipulation, dynamic deserialization, or complex ContractResolver customisation, the migration cost may outweigh the benefits. Both libraries can coexist — ASP.NET Core lets you configure either as the default serializer.
Wrapping up
The migration is straightforward for most DTOs and gets progressively harder with custom converters and polymorphic hierarchies. Start with your simplest models, verify behaviour with integration tests, and work outwards. The performance gains and reduced dependency footprint make it worthwhile for the majority of projects.