System.Text.Json in .NET 10: Security, Performance, and Modern JSON Handling

System.Text.Json has been .NET's high-performance JSON serializer since .NET Core 3.0, but with .NET 10, it reaches a new level of maturity. This release brings critical security hardening, performance optimizations, and developer-friendly features that address real-world production challenges.
If you've been using Newtonsoft.Json (JSON.NET) or struggled with JSON security vulnerabilities, this article is for you.
Why System.Text.Json Matters
Since its introduction in .NET Core 3.0, System.Text.Json has been designed from the ground up for:
✅ High performance - Zero allocations, UTF-8 native processing
✅ Modern async patterns - First-class Stream and async support
✅ Security by default - Protection against common JSON attacks
✅ Source generators - Compile-time serialization for AOT scenarios
While Newtonsoft.Json remains popular, Microsoft's strategic direction is clear: System.Text.Json is the future. With .NET 10, the gap between the two has narrowed significantly.
What's New in .NET 10?
The .NET 10 release introduces five major improvements to System.Text.Json:
- Duplicate Property Detection - Protection against JSON payload attacks
- Strict Serialization Preset - Secure-by-default configuration bundling all security features
- PipeReader Support - Efficient streaming without intermediate buffers
- Performance Improvements - Faster serialization and reduced allocations
- Better Nullable Reference Type Support - Enhanced type safety
Let's dive into each feature with practical examples.
1. Duplicate Property Detection: Preventing Payload Attacks
The Problem
JSON allows duplicate keys, but the behavior is undefined in the specification. Different parsers handle duplicates differently:
- Newtonsoft.Json - Uses the last value
- System.Text.Json (.NET 9 and earlier) - Uses the last value
- Some parsers - Throw errors
- Attackers - Exploit the ambiguity
Where This Comes From
This feature addresses CVE-2023-XXXXX class vulnerabilities where attackers use duplicate properties to bypass validation:
{
"userId": 1,
"userId": 999,
"role": "user",
"role": "admin"
}
A validator checking the first value sees userId: 1 and role: "user", but the deserializer uses the last values: 999 and "admin".
Real-World Example: Authentication Bypass Attack
Consider this vulnerable code:
public class LoginRequest { public string Username { get; set; } public string Password { get; set; } public bool IsAdmin { get; set; } = false; // Should never be true from client } // WITHOUT duplicate detection - VULNERABLE var loginRequest = JsonSerializer.Deserialize<LoginRequest>(json); // Malicious payload: // {"username":"hacker","password":"pass","isAdmin":true,"isAdmin":false} // Last value wins - but validation only checked the first value! // Result: attacker bypasses IsAdmin protection!
The .NET 10 Solution
Fine-grained control over duplicate property handling:
var options = new JsonSerializerOptions { AllowDuplicateProperties = false // New in .NET 10 }; try { var user = JsonSerializer.Deserialize<User>(maliciousJson, options); } catch (JsonException ex) { // "Duplicate property 'userId' detected at line 2, position 12" _logger.LogWarning(ex, "Rejected malicious JSON payload"); }
Now with duplicate detection enabled:
// WITH duplicate detection - SECURE var options = new JsonSerializerOptions { AllowDuplicateProperties = false }; var loginRequest = JsonSerializer.Deserialize<LoginRequest>(json, options); // Throws JsonException: Duplicate property 'isAdmin' detected
Practical Example: API Input Validation
[ApiController] [Route("api/[controller]")] public class UsersController : ControllerBase { private static readonly JsonSerializerOptions _secureOptions = new() { AllowDuplicateProperties = false, UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow }; [HttpPost] public async Task<IActionResult> CreateUser() { try { // Read request body with duplicate detection var user = await JsonSerializer.DeserializeAsync<CreateUserRequest>( Request.Body, _secureOptions ); // Safe to process - no duplicate properties var result = await _userService.CreateUserAsync(user); return Ok(result); } catch (JsonException ex) { // Log security event _logger.LogWarning(ex, "Rejected malformed JSON from {IP}", HttpContext.Connection.RemoteIpAddress); return BadRequest("Invalid JSON payload"); } } }
Performance Impact
Minimal overhead - Duplicate detection uses a hash set to track property names, adding approximately 2-5% overhead for typical payloads. The security benefits far outweigh the cost.
2. Strict Serialization Preset: Security by Default
The Problem
While individual security features like duplicate detection are powerful, developers often overlook multiple security configurations, leaving applications vulnerable to:
❌ Duplicate properties - Overwriting security fields
❌ Unknown properties - Injecting unexpected data
❌ Case-insensitive matching - Bypassing validation
❌ Null handling - Type confusion attacks
Manually configuring all security options is tedious and error-prone.
Where This Comes From
The strict preset was introduced in response to real-world security incidents where JSON deserialization vulnerabilities led to authentication bypasses, privilege escalation, and data leaks. Microsoft worked with security researchers to create a secure-by-default configuration that bundles all protective features.
The .NET 10 Solution
A new JsonSerializerOptions.Strict preset that enables all security features with a single line:
using System.Text.Json; // Secure-by-default configuration var options = JsonSerializerOptions.Strict; var user = JsonSerializer.Deserialize<User>(jsonPayload, options);
What does Strict enable?
// Equivalent to manually configuring: var options = new JsonSerializerOptions { AllowDuplicateProperties = false, // Reject duplicate keys (from section 1) UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, // Reject unknown properties PropertyNameCaseInsensitive = false, // Enforce exact casing RespectNullableAnnotations = true, // Honor nullable reference types RespectRequiredConstructorParameters = true // Enforce required parameters };
Complete Security Example
public class LoginRequest { public required string Username { get; init; } public required string Password { get; init; } public bool IsAdmin { get; init; } = false; } // Using strict preset for complete protection var options = JsonSerializerOptions.Strict; // ❌ Rejects duplicate properties var payload1 = """{"username":"user","password":"pass","isAdmin":true,"isAdmin":false}"""; // Throws: Duplicate property 'isAdmin' detected // ❌ Rejects unknown properties var payload2 = """{"username":"user","password":"pass","extraField":"hack"}"""; // Throws: Unmapped member 'extraField' // ❌ Rejects wrong casing (if PropertyNameCaseInsensitive = false) var payload3 = """{"USERNAME":"user","password":"pass"}"""; // Throws: Missing required property 'Username' // ❌ Rejects missing required properties var payload4 = """{"username":"user"}"""; // Throws: Required property 'Password' missing // ✅ Accepts only valid, secure payloads var payload5 = """{"username":"user","password":"pass"}"""; var request = JsonSerializer.Deserialize<LoginRequest>(payload5, options); // Works!
When to Use Strict Mode
✅ API endpoints receiving untrusted JSON
✅ Authentication/authorization payloads
✅ Configuration files from external sources
✅ Webhook handlers processing third-party data
✅ Any public-facing JSON input
⚠️ When NOT to use:
- Deserializing JSON from trusted, well-controlled sources
- Working with legacy APIs that rely on case-insensitive matching
- Scenarios requiring backward compatibility with loose JSON
- Internal microservice communication where performance is critical
3. PipeReader Support: Zero-Copy Deserialization
The Problem
Before .NET 10, deserializing JSON required converting a PipeReader to a Stream:
// OLD APPROACH - inefficient var pipe = GetPipeReader(); var stream = pipe.AsStream(); // Creates intermediate buffer var data = await JsonSerializer.DeserializeAsync<MyData>(stream);
This approach:
- Allocates an intermediate
Streamwrapper - Copies data from pipe buffers to stream buffers
- Reduces performance in high-throughput scenarios
Where This Comes From
System.IO.Pipelines is the modern, high-performance I/O API in .NET, used by:
- ASP.NET Core (Kestrel web server)
- SignalR
- gRPC
- Custom networking code
The lack of direct PipeReader support in System.Text.Json created a performance gap.
The .NET 10 Solution
Native PipeReader deserialization with zero intermediate allocations:
using System.IO.Pipelines; using System.Text.Json; // Direct deserialization from PipeReader PipeReader reader = GetPipeReader(); var person = await JsonSerializer.DeserializeAsync<Person>(reader);
Real-World Example: High-Performance API Gateway
public class JsonApiHandler { public async Task<T> ProcessRequestAsync<T>(PipeReader reader) { // Zero-copy deserialization var request = await JsonSerializer.DeserializeAsync<T>(reader); // Process request return request; } }
Streaming Large JSON Files
public async Task<List<Product>> LoadProductsFromFileAsync(string filePath) { var pipe = new Pipe(); // Producer: Read file into pipe var writingTask = Task.Run(async () => { await using var fileStream = File.OpenRead(filePath); await fileStream.CopyToAsync(pipe.Writer); await pipe.Writer.CompleteAsync(); }); // Consumer: Deserialize directly from PipeReader var products = await JsonSerializer.DeserializeAsync<List<Product>>(pipe.Reader); await writingTask; return products; }
Performance Comparison
**Benchmark: In-Comming **
4. Performance Improvements
Reduced Allocations
.NET 10 introduces several micro-optimizations:
String pooling for property names:
// .NET 9: Allocates new string for each property // .NET 10: Uses string interning for common property names var json = """{"Name":"John","Age":30}"""; var person = JsonSerializer.Deserialize<Person>(json); // Fewer allocations
Optimized UTF-8 handling:
// .NET 10: Faster UTF-8 validation and conversion byte[] utf8Json = Encoding.UTF8.GetBytes(jsonString); var person = JsonSerializer.Deserialize<Person>(utf8Json); // 15% faster
Faster Number Parsing
Improved integer and floating-point parsing:
public record Metrics(long Requests, double AverageLatency, decimal TotalCost); // .NET 10: Optimized number parsing with SIMD instructions var metrics = JsonSerializer.Deserialize<Metrics>(json); // 20% faster
5. Enhanced Nullable Reference Type Support
The Problem
C# nullable reference types (string?) improve type safety, but System.Text.Json didn't always respect them during deserialization.
public class User { public string Name { get; set; } // Non-nullable, but JSON can provide null! public string? MiddleName { get; set; } // Nullable, null is OK }
The .NET 10 Solution
RespectNullableAnnotations option enforces nullable annotations:
var options = new JsonSerializerOptions { RespectNullableAnnotations = true }; // This throws because Name is non-nullable var json = """{"Name":null,"MiddleName":"John"}"""; var user = JsonSerializer.Deserialize<User>(json, options); // Throws JsonException: Cannot assign null to non-nullable property 'Name'
Practical Example: Input Validation
public record CreateProductRequest { public required string Name { get; init; } // Must be present and non-null public string? Description { get; init; } // Optional public required decimal Price { get; init; } // Must be present } var options = new JsonSerializerOptions { RespectNullableAnnotations = true, RespectRequiredConstructorParameters = true }; // Valid JSON var valid = """{"Name":"Widget","Price":9.99}"""; var product = JsonSerializer.Deserialize<CreateProductRequest>(valid, options); // ✅ Works // Invalid JSON - missing required Name var invalid = """{"Price":9.99}"""; var product2 = JsonSerializer.Deserialize<CreateProductRequest>(invalid, options); // ❌ Throws
Conclusion
.NET 10 makes System.Text.Json production-ready for even the most demanding scenarios. The combination of:
✅ Strict security presets - Protection against JSON attacks
✅ Duplicate property detection - Prevent authentication bypasses
✅ PipeReader support - Zero-copy high-performance deserialization
✅ Performance improvements - 15-20% faster with 25% less allocation
✅ Nullable reference type support - Better type safety
...makes it the clear choice for new .NET applications.
Resources
- 📚 System.Text.Json Documentation
- 🚀 .NET 10 What's New
- 🔒 JSON Security Best Practices
- 💬 .NET Discord Community
The future of JSON in .NET is fast, secure, and ready for production. Time to upgrade!
See you on www.devskillsunlock.com for more .NET!
Related Articles:
