惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - Franky
云风的 BLOG
云风的 BLOG
人人都是产品经理
人人都是产品经理
博客园 - 叶小钗
Engineering at Meta
Engineering at Meta
Vercel News
Vercel News
Y
Y Combinator Blog
B
Blog
Microsoft Azure Blog
Microsoft Azure Blog
C
Check Point Blog
M
MIT News - Artificial intelligence
Jina AI
Jina AI
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Apple Machine Learning Research
Apple Machine Learning Research
Hugging Face - Blog
Hugging Face - Blog
阮一峰的网络日志
阮一峰的网络日志
罗磊的独立博客
Stack Overflow Blog
Stack Overflow Blog
F
Fortinet All Blogs
博客园 - 司徒正美
I
InfoQ
Google DeepMind News
Google DeepMind News
GbyAI
GbyAI
U
Unit 42

Duende Software Official Site

The Backend for Frontend Pattern Is Now Official IETF Guidance: RFC 10017 Published WhatsApp One-Time Password (OTP) Login with Duende IdentityServer and User Management Planning a Successful Migration from IdentityServer3 to Duende IdentityServer Client Secrets, Mutual TLS and Private Key JWT, Oh My! How To Spell "Duende" Understanding .NET 11 Automatic CSRF Protection: A Guide for Identity Developers Security Lingo Explained: TOTP (Time-based One-Time Password) Custom Passkey Attestation Policies: Restricting Login to Hardware Keys OAuth Identity Chaining, Transaction Tokens, and Human-in-the-Loop: Summer 2026 Identity Standards Recap What is Identity? - The Question Every Team Should Answer Before Writing Code Security Is a Spectrum: How to Choose Session Lifetimes in Duende IdentityServer Passkeys and WebAuthn with Duende IdentityServer and User Management Authenticating Players in Godot 4 with OAuth 2.0 and OpenID Connect Hardening OAuth in the newest 2026-07-28 MCP Release Candidate Unify Your SAML and OIDC Signing Keys with Automatic Rotation and Duende IdentityServer Duende Software Duende Software Duende Software Duende Software Duende Software Duende Software Duende Software Duende Software Stop AI Bots from Wasting Your Server How Duende IdentityServer Filters Claims (And Why It Matters) Core vs Extended Protocols in Duende IdentityServer v8: What You Get and When You Need More Your IdentityServer v8 Upgrade Checklist: A Quick Pre-Flight Guide Setting Up SAML Single Sign-On in ASP.NET with Duende IdentityServer Your Identity, Your Terms: Duende's Modular Identity Infrastructure and v8.x Release Duende Spring Launch '26: Identity Infrastructure That Expands With You
Harden Your .NET JSON Deserialization with System.Text.Js...
Khalid Abuhakmeh · 2026-04-30 · via Duende Software Official Site

Consider the following JSON payload hitting your API:

{"Amount": 100, "Amount": -999}

Two properties with the same name. RFC 8259 Section 4 says object names "SHOULD be unique," and warns that parser behavior is unpredictable when they aren't. System.Text.Json takes the permissive path: last-write-wins, no warning, no error.

Csharp

using System.Text.Json;

string duplicateJson = """{"Amount": 100, "Amount": -999}""";

var payment = JsonSerializer.Deserialize<Payment>(duplicateJson);
Console.WriteLine($"Amount = {payment!.Amount}");
// Output: Amount = -999

public record Payment(int Amount);

The attacker's value wins silently. This isn't just a duplicate property quirk. Default deserialization also ignores extra fields an attacker might inject, lets null slide into non-nullable properties, and skips over missing required data. Each of these "conveniences" is a potential security gap at your API boundary.

What JsonSerializerOptions.Strict Does

.NET 10 introduces JsonSerializerOptions.Strict, a new read-only preset that sits alongside Default and Web. Where Default prioritizes backward compatibility, and Web optimizes for typical HTTP APIs, Strict follows security best practices.

Csharp

using System.Text.Json;

var strict = JsonSerializerOptions.Strict;
Console.WriteLine($"AllowDuplicateProperties:             {strict.AllowDuplicateProperties}");
Console.WriteLine($"UnmappedMemberHandling:               {strict.UnmappedMemberHandling}");
Console.WriteLine($"PropertyNameCaseInsensitive:           {strict.PropertyNameCaseInsensitive}");
Console.WriteLine($"RespectNullableAnnotations:            {strict.RespectNullableAnnotations}");
Console.WriteLine($"RespectRequiredConstructorParameters:  {strict.RespectRequiredConstructorParameters}");

Output:

AllowDuplicateProperties:             False
UnmappedMemberHandling:               Disallow
PropertyNameCaseInsensitive:           False
RespectNullableAnnotations:            True
RespectRequiredConstructorParameters:  True

Five properties, five protections. Here's what each one catches.

Strict vs Default vs Web

Setting Default Web Strict
AllowDuplicateProperties true true false
UnmappedMemberHandling Skip Skip Disallow
PropertyNameCaseInsensitive false true false
RespectNullableAnnotations false false true
RespectRequiredConstructorParameters false false true

One thing to note: data serialized with Default options can be deserialized with Strict. The compatibility goes in one direction. Strict is stricter about what it accepts, not about what it produces.

Disallowing Duplicate Properties

Duplicate JSON properties are the opening example because they're the most surprising. Protocols that layer JSON parsing (OAuth 2.0, OpenID Connect, webhook signatures) can be exploited if different parsers handle duplicate inputs differently. One parser sees the first value, another sees the last.

Csharp

using System.Text.Json;

string duplicateJson = """{"Amount": 100, "Amount": -999}""";

try
{
    JsonSerializer.Deserialize<Payment>(duplicateJson, JsonSerializerOptions.Strict);
}
catch (JsonException ex)
{
    Console.WriteLine($"JsonException: {ex.Message}");
}
// Output: JsonException: Duplicate property 'Amount' encountered during
//         deserialization of type 'Payment'.

public record Payment(int Amount);

This protection extends beyond POCO (plain-old C# objects) deserialization. You can catch duplicates in JsonDocument, JsonNode, and Dictionary<string, T> too:

Csharp

using System.Text.Json;
using System.Text.Json.Nodes;

string dupeJson = """{"Key": "first", "Key": "second"}""";
var noDupeOptions = new JsonSerializerOptions { AllowDuplicateProperties = false };

// JsonDocument uses its own options type
var docOptions = new JsonDocumentOptions { AllowDuplicateProperties = false };
using var doc = JsonDocument.Parse(dupeJson, docOptions); // throws JsonException

// JsonNode inherits the setting from JsonDocumentOptions
var node = JsonNode.Parse(dupeJson, nodeOptions: null, docOptions); // throws JsonException

// Dictionary catches it through JsonSerializerOptions
JsonSerializer.Deserialize<Dictionary<string, string>>(dupeJson, noDupeOptions); // throws JsonException

All three throw JsonException on the duplicate.

Rejecting Unmapped Members

Default deserialization silently drops JSON properties that don't map to your .NET type. That's convenient during development. It's dangerous at a trust boundary.

Csharp

using System.Text.Json;

string extraFieldJson = """{"Name": "Alice", "Role": "admin", "IsRoot": true}""";

// Default: silently ignores "IsRoot"
var user = JsonSerializer.Deserialize<User>(extraFieldJson);
Console.WriteLine($"Name={user!.Name}, Role={user.Role}");
// Output: Name=Alice, Role=admin — "IsRoot" vanished without a trace

// Strict: rejects the unmapped property
JsonSerializer.Deserialize<User>(extraFieldJson, JsonSerializerOptions.Strict);
// throws: The JSON property 'IsRoot' could not be mapped to any .NET member
//         contained in type 'User'.

public record User(string Name, string Role);

This catches a class of bugs where clients send fields that your API doesn't process. If you're not rejecting them, you don't know they're there.

UnmappedMemberHandling was introduced in .NET 8, so you may have already opted into this with JsonUnmappedMemberHandling.Disallow. The Strict preset bundles it with the other protections.

Case-Sensitive Property Matching

The Web preset sets PropertyNameCaseInsensitive = true so that "username" matches a C# property named Username. That's helpful for typical JavaScript-to-.NET communication. But combined with UnmappedMemberHandling.Disallow in Strict mode, case sensitivity becomes a precise contract: JSON property names must match C# property names exactly, or the deserializer rejects them.

Csharp

using System.Text.Json;

string camelCaseJson = """{"username": "bob"}""";

// Web: case-insensitive, "username" matches "Username"
var webResult = JsonSerializer.Deserialize<LoginRequest>(
    camelCaseJson, JsonSerializerOptions.Web);
Console.WriteLine($"Username = \"{webResult!.Username}\"");
// Output: Username = "bob"

// Strict: case-sensitive + unmapped member detection
JsonSerializer.Deserialize<LoginRequest>(
    camelCaseJson, JsonSerializerOptions.Strict);
// throws: The JSON property 'username' could not be mapped to any .NET member
//         contained in type 'LoginRequest'.

// PascalCase matches exactly
string pascalCaseJson = """{"Username": "bob"}""";
var strictResult = JsonSerializer.Deserialize<LoginRequest>(
    pascalCaseJson, JsonSerializerOptions.Strict);
Console.WriteLine($"Username = \"{strictResult!.Username}\"");
// Output: Username = "bob"

public record LoginRequest(string Username);

If your clients send camelCase and you want strict validation, add [JsonPropertyName("username")] to your properties. That way the contract is explicit in the type definition rather than implicit in the options.

Enforcing Nullable Annotations

C# nullable reference types help you catch null issues at compile time. But System.Text.Json ignores them by default during deserialization. A JSON null slides right into a string property without complaint.

Csharp

using System.Text.Json;

string nullNameJson = """{"Name": null, "Email": "alice@example.com"}""";

// Default: null goes into non-nullable string
var contact = JsonSerializer.Deserialize<Contact>(nullNameJson);
Console.WriteLine($"Name = {(contact!.Name is null ? "null" : contact.Name)}");
// Output: Name = null

// Strict: rejects null for non-nullable property
JsonSerializer.Deserialize<Contact>(nullNameJson, JsonSerializerOptions.Strict);
// throws: The constructor parameter 'Name' on type 'Contact' doesn't allow null
//         values. Consider updating its nullability annotation. // ...

public record Contact(string Name, string Email);

RespectNullableAnnotations was added in .NET 9. In Strict mode, the serializer enforces your nullable annotations at the deserialization boundary. If you declared string Name (not string? Name), the serializer holds you to it.

Requiring Constructor Parameters

Record types and classes with parameterized constructors can have required parameters silently filled with default values when JSON is missing the data.

Csharp

using System.Text.Json;

string missingParamJson = """{"FirstName": "Alice"}""";

// Default: missing "LastName" silently becomes null
var person = JsonSerializer.Deserialize<Person>(missingParamJson);
Console.WriteLine($"FirstName={person!.FirstName}, LastName={person.LastName ?? "null"}");
// Output: FirstName=Alice, LastName=null

// Strict: requires all constructor parameters
JsonSerializer.Deserialize<Person>(missingParamJson, JsonSerializerOptions.Strict);
// throws: JSON deserialization for type 'Person' was missing required properties
//         including: 'LastName'.

public record Person(string FirstName, string LastName);

This catches partial payloads that would otherwise result in objects being in an invalid state. When combined with nullable annotation enforcement, this means your deserialized objects match your type's declared contract.

Strict Mode with Source Generators

JsonSerializerOptions.Strict uses reflection-based serialization. For AOT scenarios or when you want the performance benefits of source generators, you need to configure the equivalent settings manually on JsonSourceGenerationOptionsAttribute:

Csharp

using System.Text.Json;
using System.Text.Json.Serialization;

string validJson = """{"Amount": 42}""";
string invalidJson = """{"Amount": 42, "Amount": 99}""";

var validPayment = JsonSerializer.Deserialize(
    validJson, StrictJsonContext.Default.Payment);
Console.WriteLine($"Amount = {validPayment!.Amount}");
// Output: Amount = 42

JsonSerializer.Deserialize(
    invalidJson, StrictJsonContext.Default.Payment);
// throws: Duplicate property 'Amount' encountered during
//         deserialization of type 'Payment'.

public record Payment(int Amount);

[JsonSourceGenerationOptions(
    AllowDuplicateProperties = false,
    UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
    PropertyNameCaseInsensitive = false,
    RespectNullableAnnotations = true,
    RespectRequiredConstructorParameters = true
)]
[JsonSerializable(typeof(Payment))]
internal partial class StrictJsonContext : JsonSerializerContext;

There's no single Strict shorthand for the attribute. You set each property individually. The generated code includes all validation logic at compile time with no reflection overhead.

Using Strict Options in ASP.NET Core Minimal APIs

The demos above use JsonSerializer directly. In a web application, you configure JSON options once, and every endpoint inherits them.

Configuring Globally

Csharp

using System.Text.Json;

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.AllowDuplicateProperties = false;
    options.SerializerOptions.UnmappedMemberHandling =
        System.Text.Json.Serialization.JsonUnmappedMemberHandling.Disallow;
    options.SerializerOptions.PropertyNameCaseInsensitive = false;
    options.SerializerOptions.RespectNullableAnnotations = true;
    options.SerializerOptions.RespectRequiredConstructorParameters = true;
});

var app = builder.Build();

app.MapPost("/payments", (Payment payment) =>
{
    // If the request body has duplicate properties, unmapped fields,
    // or missing required data, the framework returns 400 Bad Request
    // before this code ever runs.
    return Results.Ok(payment);
});

app.Run();

public record Payment(int Amount);

The framework catches JsonException during model binding and returns a 400 Bad Request with problem details. Your endpoint code only sees valid, fully-bound objects.

Note that JsonSerializerOptions.Strict is a frozen singleton, so you can't pass it directly to ConfigureHttpJsonOptions (which needs a mutable instance). Copy the individual settings as shown above, or create a new instance from the preset and customize:

Csharp

builder.Services.ConfigureHttpJsonOptions(options =>
{
    // Start from Strict defaults, then adjust
    var strictBase = new JsonSerializerOptions(JsonSerializerOptions.Strict)
    {
        // Add camelCase naming if your clients expect it
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        PropertyNameCaseInsensitive = true
    };

    options.SerializerOptions.AllowDuplicateProperties = strictBase.AllowDuplicateProperties;
    options.SerializerOptions.UnmappedMemberHandling = strictBase.UnmappedMemberHandling;
    options.SerializerOptions.RespectNullableAnnotations = strictBase.RespectNullableAnnotations;
    options.SerializerOptions.RespectRequiredConstructorParameters = strictBase.RespectRequiredConstructorParameters;
    options.SerializerOptions.PropertyNameCaseInsensitive = strictBase.PropertyNameCaseInsensitive;
    options.SerializerOptions.PropertyNamingPolicy = strictBase.PropertyNamingPolicy;
});

Per-Endpoint Options

If you need strict validation on some endpoints but relaxed parsing on others, use Results.Json with explicit options:

Csharp

app.MapGet("/api/data", () =>
    Results.Json(new { message = "Hello" }, JsonSerializerOptions.Strict));

For deserialization, bind from the raw request body with your own options:

Csharp

app.MapPost("/api/strict", async (HttpContext context) =>
{
    var payment = await context.Request.ReadFromJsonAsync<Payment>(
        JsonSerializerOptions.Strict);
    return Results.Ok(payment);
});

When to Use Strict (and When Not To)

Use Strict at trust boundaries. Token endpoints, webhook receivers, API controllers, anything where you're accepting JSON from a client you don't fully control. The cost is a JsonException when payloads don't match your contract. That's the point.

Think twice for flexible ingestion. If you're consuming JSON from third-party APIs with inconsistent schemas, strict mode will reject payloads you might want to handle gracefully. In those cases, use Default or Web and validate after deserialization.

Migrate incrementally. You don't need to flip everything to Strict at once. Start with your highest-risk endpoints. Catch JsonException, log it, fix the callers that send non-conforming payloads, then expand.

Know what Strict doesn't cover. This preset validates structural contract violations: duplicates, unmapped fields, null mismatches, and missing data. It doesn't protect against deeply nested JSON (use MaxDepth), oversized payloads (handle at the HTTP layer with request size limits), or polymorphic type confusion. Strict mode is one layer of defense, not the only one.

Tightening Your Security Surface

Every API endpoint that accepts JSON is a trust boundary. Permissive deserialization makes that boundary porous: duplicate properties slip through, unmapped fields go unnoticed, null values bypass your type system, and missing data produces half-initialized objects.

JsonSerializerOptions.Strict doesn't add new validation logic. It activates protections that already exist in System.Text.Json but are off by default for backward compatibility. One line of configuration turns them all on.

This matters especially at protocol boundaries. Duende IdentityServer processes OAuth 2.0 and OpenID Connect (OIDC) JSON payloads at the token, introspection, and dynamic client registration endpoints. These are high-value targets where a duplicate property or an unexpected field isn't just a bug; it's a potential exploit vector. Strict JSON validation is defense-in-depth for any system that processes protocol messages.

Try It Yourself

Everything below is the complete project. Create a directory, drop these three files into it, and run it. There are no external dependencies beyond the .NET 10 SDK.

JsonStrictOptions.csproj

Markup

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>14</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

Program.cs

Csharp

using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;

Console.WriteLine("=== .NET 10: JsonSerializerOptions.Strict ===\n");

// ---------------------------------------------------------------
// Demo 1: Duplicate Properties — The Silent Override
// ---------------------------------------------------------------
Console.WriteLine("--- Demo 1: Duplicate Properties ---");

string duplicateJson = """{"Amount": 100, "Amount": -999}""";

// Default behavior: last-write-wins, silently
var payment = JsonSerializer.Deserialize<Payment>(duplicateJson);
Console.WriteLine($"Default options:  Amount = {payment!.Amount}");
// The attacker's value wins — no warning, no error

// Strict catches the duplicate immediately
try
{
    JsonSerializer.Deserialize<Payment>(duplicateJson, JsonSerializerOptions.Strict);
}
catch (JsonException ex)
{
    Console.WriteLine($"Strict options:   JsonException — {ex.Message}");
}

Console.WriteLine();

// ---------------------------------------------------------------
// Demo 2: Strict Preset — All Five Protections
// ---------------------------------------------------------------
Console.WriteLine("--- Demo 2: Strict Preset Properties ---");

var strict = JsonSerializerOptions.Strict;
Console.WriteLine($"AllowDuplicateProperties:             {strict.AllowDuplicateProperties}");
Console.WriteLine($"UnmappedMemberHandling:               {strict.UnmappedMemberHandling}");
Console.WriteLine($"PropertyNameCaseInsensitive:           {strict.PropertyNameCaseInsensitive}");
Console.WriteLine($"RespectNullableAnnotations:            {strict.RespectNullableAnnotations}");
Console.WriteLine($"RespectRequiredConstructorParameters:  {strict.RespectRequiredConstructorParameters}");

Console.WriteLine();

// ---------------------------------------------------------------
// Demo 3: Unmapped Members — No Sneaking Extra Fields
// ---------------------------------------------------------------
Console.WriteLine("--- Demo 3: Unmapped Member Detection ---");

string extraFieldJson = """{"Name": "Alice", "Role": "admin", "IsRoot": true}""";

// Default: silently ignores "IsRoot"
var userDefault = JsonSerializer.Deserialize<User>(extraFieldJson);
Console.WriteLine($"Default options:  deserialized OK — Name={userDefault!.Name}, Role={userDefault.Role}");

// Strict: rejects the unmapped "IsRoot" property
try
{
    JsonSerializer.Deserialize<User>(extraFieldJson, JsonSerializerOptions.Strict);
}
catch (JsonException ex)
{
    Console.WriteLine($"Strict options:   JsonException — {ex.Message}");
}

Console.WriteLine();

// ---------------------------------------------------------------
// Demo 4: Case Sensitivity — Exact Property Matching
// ---------------------------------------------------------------
Console.WriteLine("--- Demo 4: Case Sensitivity ---");

// JSON uses camelCase, but the C# record property is PascalCase "Username"
string camelCaseJson = """{"username": "bob"}""";

// Web preset: PropertyNameCaseInsensitive = true, so "username" matches "Username"
var webResult = JsonSerializer.Deserialize<LoginRequest>(camelCaseJson, JsonSerializerOptions.Web);
Console.WriteLine($"Web options:      Username = \"{webResult!.Username}\" (case-insensitive match)");

// Strict preset: PropertyNameCaseInsensitive = false + UnmappedMemberHandling = Disallow
// "username" does not match "Username", and the unmapped property throws
try
{
    JsonSerializer.Deserialize<LoginRequest>(camelCaseJson, JsonSerializerOptions.Strict);
}
catch (JsonException ex)
{
    Console.WriteLine($"Strict options:   JsonException — {ex.Message}");
}

// PascalCase JSON matches the C# property name exactly
string pascalCaseJson = """{"Username": "bob"}""";
var strictResult = JsonSerializer.Deserialize<LoginRequest>(pascalCaseJson, JsonSerializerOptions.Strict);
Console.WriteLine($"Strict + exact:   Username = \"{strictResult!.Username}\" (exact case match)");

Console.WriteLine();

// ---------------------------------------------------------------
// Demo 5: Nullable Annotations — Null Safety at the Boundary
// ---------------------------------------------------------------
Console.WriteLine("--- Demo 5: Nullable Annotation Enforcement ---");

string nullNameJson = """{"Name": null, "Email": "alice@example.com"}""";

// Default: allows null into non-nullable string
var contactDefault = JsonSerializer.Deserialize<Contact>(nullNameJson);
Console.WriteLine($"Default options:  Name = {(contactDefault!.Name is null ? "null" : contactDefault.Name)} (allowed!)");

// Strict: rejects null for non-nullable property
try
{
    JsonSerializer.Deserialize<Contact>(nullNameJson, JsonSerializerOptions.Strict);
}
catch (JsonException ex)
{
    Console.WriteLine($"Strict options:   JsonException — {ex.Message}");
}

Console.WriteLine();

// ---------------------------------------------------------------
// Demo 6: Required Constructor Parameters
// ---------------------------------------------------------------
Console.WriteLine("--- Demo 6: Required Constructor Parameters ---");

string missingParamJson = """{"FirstName": "Alice"}""";

// Default: missing "LastName" silently becomes null
var personDefault = JsonSerializer.Deserialize<Person>(missingParamJson);
Console.WriteLine($"Default options:  FirstName={personDefault!.FirstName}, LastName={(personDefault.LastName is null ? "null" : personDefault.LastName)}");

// Strict: missing required constructor parameter throws
try
{
    JsonSerializer.Deserialize<Person>(missingParamJson, JsonSerializerOptions.Strict);
}
catch (JsonException ex)
{
    Console.WriteLine($"Strict options:   JsonException — {ex.Message}");
}

Console.WriteLine();

// ---------------------------------------------------------------
// Demo 7: Duplicates Across Types — JsonDocument, JsonNode, Dictionary
// ---------------------------------------------------------------
Console.WriteLine("--- Demo 7: Duplicate Detection Across Types ---");

string dupeJson = """{"Key": "first", "Key": "second"}""";
var noDupeOptions = new JsonSerializerOptions { AllowDuplicateProperties = false };

// JsonDocument
try
{
    var docOptions = new JsonDocumentOptions { AllowDuplicateProperties = false };
    using var doc = JsonDocument.Parse(dupeJson, docOptions);
}
catch (JsonException ex)
{
    Console.WriteLine($"JsonDocument:     JsonException — {ex.Message}");
}

// JsonNode (via JsonDocument parsing options)
try
{
    var docOptions2 = new JsonDocumentOptions { AllowDuplicateProperties = false };
    var node = JsonNode.Parse(dupeJson, nodeOptions: null, docOptions2);
}
catch (JsonException ex)
{
    Console.WriteLine($"JsonNode:         JsonException — {ex.Message}");
}

// Dictionary<string, string>
try
{
    JsonSerializer.Deserialize<Dictionary<string, string>>(dupeJson, noDupeOptions);
}
catch (JsonException ex)
{
    Console.WriteLine($"Dictionary:       JsonException — {ex.Message}");
}

Console.WriteLine();

// ---------------------------------------------------------------
// Demo 8: Source Generator with Strict-Equivalent Options
// ---------------------------------------------------------------
Console.WriteLine("--- Demo 8: Source Generator (AOT-Compatible) ---");

string validJson = """{"Amount": 42}""";
string invalidJson = """{"Amount": 42, "Amount": 99}""";

var validPayment = JsonSerializer.Deserialize(validJson, StrictJsonContext.Default.Payment);
Console.WriteLine($"Valid payload:    Amount = {validPayment!.Amount}");

try
{
    JsonSerializer.Deserialize(invalidJson, StrictJsonContext.Default.Payment);
}
catch (JsonException ex)
{
    Console.WriteLine($"Duplicate payload: JsonException — {ex.Message}");
}

Console.WriteLine();
Console.WriteLine("=== All demos complete ===");

// ---------------------------------------------------------------
// Types
// ---------------------------------------------------------------

public record Payment(int Amount);

public record User(string Name, string Role);

public record LoginRequest(string Username);

public record Contact(string Name, string Email);

public record Person(string FirstName, string LastName);

// ---------------------------------------------------------------
// Source Generator Context — Strict-equivalent settings for AOT
// ---------------------------------------------------------------

[JsonSourceGenerationOptions(
    AllowDuplicateProperties = false,
    UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
    PropertyNameCaseInsensitive = false,
    RespectNullableAnnotations = true,
    RespectRequiredConstructorParameters = true
)]
[JsonSerializable(typeof(Payment))]
internal partial class StrictJsonContext : JsonSerializerContext;

Build and run with dotnet run.

Sample Output

=== .NET 10: JsonSerializerOptions.Strict ===

--- Demo 1: Duplicate Properties ---
Default options:  Amount = -999
Strict options:   JsonException — Duplicate property 'Amount' encountered during deserialization of type 'Payment'.

--- Demo 2: Strict Preset Properties ---
AllowDuplicateProperties:             False
UnmappedMemberHandling:               Disallow
PropertyNameCaseInsensitive:           False
RespectNullableAnnotations:            True
RespectRequiredConstructorParameters:  True

--- Demo 3: Unmapped Member Detection ---
Default options:  deserialized OK — Name=Alice, Role=admin
Strict options:   JsonException — The JSON property 'IsRoot' could not be mapped to any .NET member contained in type 'User'.

--- Demo 4: Case Sensitivity ---
Web options:      Username = "bob" (case-insensitive match)
Strict options:   JsonException — The JSON property 'username' could not be mapped to any .NET member contained in type 'LoginRequest'.
Strict + exact:   Username = "bob" (exact case match)

--- Demo 5: Nullable Annotation Enforcement ---
Default options:  Name = null (allowed!)
Strict options:   JsonException — The constructor parameter 'Name' on type 'Contact' doesn't allow null values. Consider updating its nullability annotation. Path: $.Name | LineNumber: 0 | BytePositionInLine: 13.

--- Demo 6: Required Constructor Parameters ---
Default options:  FirstName=Alice, LastName=null
Strict options:   JsonException — JSON deserialization for type 'Person' was missing required properties including: 'LastName'.

--- Demo 7: Duplicate Detection Across Types ---
JsonDocument:     JsonException — Duplicate property 'Key' encountered during deserialization.
JsonNode:         JsonException — Duplicate property 'Key' encountered during deserialization.
Dictionary:       JsonException — Duplicate properties not allowed during deserialization.

--- Demo 8: Source Generator (AOT-Compatible) ---
Valid payload:    Amount = 42
Duplicate payload: JsonException — Duplicate property 'Amount' encountered during deserialization of type 'Payment'.

=== All demos complete ===

JsonSerializerOptions.Strict turns on protections that should have been defaults from the start. One preset, five validations, zero runtime cost beyond the checks themselves. Start with your highest-risk endpoints and expand from there.