.NET 10: System.Text.Json Improvements
Introduction
System.Text.Json continues to evolve in .NET 10 with meaningful improvements focused on correctness and performance.
Let’s look at two areas that matter in real-world applications: duplicate property handling and PipeReader integration.
Duplicate Property Rejection
JSON technically allows duplicate properties. Historically, System.Text.Json would silently keep the last occurrence.
Exemple:
{
"id": 1,
"id": 2
}
Before .NET 10, this would deserialize without error, resulting in:
id = 2;
This behavior could hide bugs or introduce subtle security risks. With .NET 10, you can explicitly reject duplicate properties. Now, a JsonException is thrown when duplicates are detected, giving you stricter input validation and more predictable behavior:
Pretty useful right?
PipeReader Integration
High-performance applications often use System.IO.Pipelines. However, prior to .NET 10, System.Text.Json did not provide a direct overload accepting aPipeReader.
Developers typically had to bridge the pipeline to a Stream:
These approaches worked but:
- Added extra complexity
- Introduced bridging layers
- Increased the risk of subtle buffering bugs
.NET 10 simplifies this dramatically by allowing direct deserialization from a PipeReader:
- No stream adapter.
- No manual reader loop.
- No extra plumbing.
This results in:
- Cleaner code
- Better performance characteristics
- More natural integration with ASP.NET Core and custom servers
Strict Serialization preset
Configuring stricter JSON behavior used to mean setting multiple options individually, which was easy to forget and inconsistent across codebases.
Before .NET 10, you had to wire up each option manually:
Forget one of these and your API silently accepts payloads it shouldn’t.
.NET 10 introduces JsonSerializerOptions.Strict, a ready-to-use preset that bundles all of the above into a single line:
With this preset active:
- Duplicate properties throw a JsonException
- Unknown properties are rejected
- Casing is enforced (no case-insensitive fallback)
- Nullable annotations are respected
- Required constructor parameters are enforced
One line. Secure by default. No more “did I remember to set all the flags?” anxiety.
Fine grained Source Generation control
Source generation in System.Text.Json has been available since .NET 6, giving you compile-time metadata generation for better performance and AOT compatibility. However, there was one stubborn limitation: if your object graph contained circular references, the source-generated context would throw at runtime. There was no way to configure reference handling inside a JsonSourceGenerationOptions context, you had to fall back to reflection.
Before .NET 10, handling circular references with source generation simply wasn’t possible:
.NET 10 adds ReferenceHandler support directly inside JsonSourceGenerationOptionsAttribute, giving you fine-grained control over how cycles and references are handled at the source generation level:
Now source-generated serializers can handle self-referencing or cyclically related objects without any runtime exceptions and without giving up the performance benefits of compile-time generation.
ASP.NET Core 10 JsonPatch
ASP.NET Core 10 introduces a native JsonPatch implementation built directly on top of System.Text.Json, replacing the previous Newtonsoft.Json-based approach:
It brings better performance and lower allocations for patch operations. Keep in mind though that it does not support dynamic types, so it’s not a full drop-in replacement in every scenario.
Conclusion
.NET 10 strengthens System.Text.Json in two key ways:
- Duplicate JSON properties can now be rejected explicitly
- PipeReader integration is direct and far simpler
- Strict serialization preset
- Enhanced JsonPatch
- Fine grained generation control
Small API additions ->big impact for correctness and high-performance workloads.