System.Text.Json: Why Most .NET Developers Still Struggle With It

System.Text.Json: Why Most .NET Developers Still Struggle With It

Let’s be honest. When Microsoft first dropped System.Text.Json back in the .NET Core 3.0 days, most of us just wanted it to be Newtonsoft.Json but faster. We wanted to swap a namespace and go home. But that didn't happen. Instead, we got a library that was strict—annoyingly strict—and it broke everyone's code.

It was a mess for a while.

The reality is that System.Text.Json isn't just a "high-performance" alternative to the king, James Newton-King's ubiquitous library. It's a completely different philosophy on how data should move through a CPU. If you're still treating it like a drop-in replacement, you're probably fighting the framework more than you're writing features.

The Performance Lie (And the Truth)

You’ve likely seen the benchmarks. People love to post graphs showing System.Text.Json absolutely obliterating Newtonsoft in terms of speed and memory allocation. It’s true. It is faster. But here's the catch: it's only faster if you use it the way Microsoft intended.

If you are constantly converting everything to JsonObject or using the DOM-style JsonDocument just to avoid making a POCO (Plain Old CLR Object), you're losing the lead. The library achieves its speed by using Span<T> and Utf8JsonReader under the hood. It avoids allocations like the plague. It reads UTF-8 bytes directly from the network stream without converting them to strings first. That’s the secret sauce.

But if your code is messy? The performance gains vanish.

Why Your Deserialization Kept Failing

Most developers hit a wall because of the default behavior. Newtonsoft was the "cool parent" who let you do whatever you wanted. Case-insensitive property names? Sure. Numbers quoted as strings? No problem. Missing constructors? It’ll figure it out.

System.Text.Json is more like a strict librarian.

By default, it expects an exact case match. If your JSON has firstName and your C# class has FirstName, it’ll just return null or a default value without complaining. It won't even throw an error. You just end up with empty objects and a headache. You have to explicitly tell the JsonSerializerOptions to be case-insensitive:

var options = new JsonSerializerOptions
{
    PropertyNameCaseInsensitive = true
};

This is a design choice. Case-sensitivity is faster because the serializer doesn't have to do extra string comparisons or hash lookups. But it’s a massive friction point for anyone working with legacy APIs.

The Source Generator Revolution

If you’re working on high-scale microservices or anything running on AOT (Ahead-of-Time) compilation—like the stuff we're seeing in .NET 8 and .NET 9—you need to know about Source Generators.

Reflection is slow.

In the old days, the system text json serializer would look at your class at runtime using Reflection. It would poke and prod the properties to see what’s there. This takes time and eats memory. With Source Generators, the compiler writes the serialization code for you while you’re building the app.

It looks something like this:

[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(UserRecord))]
internal partial class MyContext : JsonSerializerContext
{
}

By doing this, you're not just saving a few milliseconds. You're making your app compatible with environments where Reflection is forbidden or heavily restricted. It's a game-changer for cloud-native apps where every megabyte of RAM costs money.

Handling the "Wonky" JSON

We've all seen it. An API returns a string "123" when it should be an integer 123. Or worse, it returns an empty string "" for a null object.

👉 See also: Ancient Greece and Science: Why Modern Tech Still Owes Them Everything

Newtonsoft handled this with a shrug. System.Text.Json will blow up.

To fix this, you have to get your hands dirty with JsonConverter. It sounds intimidating, but it’s basically just a small class where you tell the serializer exactly how to read and write a specific type. If you have a weird DateTime format that doesn't follow ISO 8601, you write a converter. If you have a Boolean that comes in as "Yes" or "No," you write a converter.

Honestly, it's better this way. It forces you to define your data contract instead of relying on "magic" behavior that might change or break in the next version of a library.

The Immortality of Polymorphic Serialization

For the longest time, doing polymorphic serialization—where you have a base class Animal and you want to deserialize it into Dog or Cat based on a property—was a nightmare in the system text json serializer. You had to write hundreds of lines of boilerplate.

Microsoft finally fixed this properly in .NET 7.

Now, you can just use attributes. You tell the base class what the "type discriminator" is, and the serializer handles the rest. It’s clean, it’s fast, and it doesn't require hacky workarounds.

[JsonDerivedType(typeof(Dog), typeDiscriminator: "dog")]
[JsonDerivedType(typeof(Cat), typeDiscriminator: "cat")]
public class Animal { ... }

Security Matters More Than You Think

Why is it so strict? Security.

A lot of the "features" in older serializers were actually vulnerabilities. Features that allowed the library to instantiate any type specified in the JSON string led to some pretty nasty Remote Code Execution (RCE) bugs. By being "opt-in" and strict by default, System.Text.Json effectively shuts the door on entire classes of exploits.

It’s not just about being fast; it’s about being safe in a world where JSON is the primary attack vector for web services.

Real-World Gotchas

Here is a list of things that usually trip people up when they switch:

  1. Circular References: By default, it hates them. It will throw a JsonException. You have to configure ReferenceHandler.Preserve or ReferenceHandler.IgnoreCycles if you have a messy object graph.
  2. Read-Only Fields: It doesn't see them. You need to use properties with at least a private set or use the [JsonInclude] attribute.
  3. Char type: It treats char as a string of length 1. Seems obvious, but it can catch you off guard if you're coming from a language that treats them as integers.
  4. Enums: They get serialized as numbers by default. Everyone hates this. Use JsonStringEnumConverter to make them human-readable strings.

The Verdict on Complexity

Is it harder to use than Newtonsoft? Yes, initially.

Is it worth the switch? Absolutely.

The .NET team is pouring all their innovation into this library. If you want to use the latest features in ASP.NET Core, or if you're eyeing Blazor WebAssembly, you really don't have a choice. The ecosystem has moved.

What’s interesting is how the community reacted. At first, there was a lot of grumbling on GitHub. Developers felt "forced" into a more difficult tool. But as the library matured, the benefits of the "Strict-First" approach became clear. Our codebases are cleaner now. We actually know what our data looks like because we can't just "guess" our way through a JSON payload anymore.

Actionable Steps for Your Next Project

If you're starting a new .NET project or refactoring an old one, don't just blindly pull in a JSON library.

  • Start with the defaults. Try to write your POCOs to match the JSON exactly. It’s the fastest path.
  • Use Source Generators. If your project is more than a simple script, the performance and AOT compatibility are worth the two minutes of setup.
  • Centralize your options. Don't create a new JsonSerializerOptions object every time you call Serialize. It's a massive performance killer because the options object caches metadata. Store it in a static variable or inject it via Dependency Injection.
  • Audit your Converters. If you have a lot of custom logic, document why. These are the places where bugs usually hide when you upgrade .NET versions.

Stop fighting the serializer. Once you accept that it’s not going to hold your hand, you can start leveraging the sheer power it offers. It’s not just about moving data anymore; it’s about moving it efficiently, safely, and predictably.

📖 Related: Finding Out How Old Is My Google Account: The Best Methods That Actually Work

The era of "magic" serialization is over. We’re in the era of explicit, high-performance data handling now. Embrace the strictness; your CPU and your security auditor will thank you for it.


Next Steps for Implementation:

  1. Identify all instances where JsonSerializer.Deserialize is called without a shared JsonSerializerOptions instance to prevent memory leaks and cache misses.
  2. Replace manual string-to-number parsing logic with custom JsonConverter<T> classes to handle dirty API data at the serialization layer.
  3. For mobile or cloud-scale applications, implement the JsonSerializerContext to enable source generation, significantly reducing startup time and memory footprint.