Harden Your .NET JSON Deserialization with System.Text.Json and JsonSerializerOptions.Strict

Khalid Abuhakmeh |

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.

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.

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.

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:

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.

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.

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.

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.

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:

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

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:

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:

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

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

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

<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

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.


Thanks for stopping by!

We hope this post helped you on your identity and security journey. If you need a hand with implementation, our docs are always open. For everything else, come hang out with the team and other developers on GitHub.

If you want to get early access to new features and products while collaborating with experts in security and identity standards, join us in our Duende Product Insiders program. And if you prefer your tech content in video form, our YouTube channel is the place to be. Don't forget to like and subscribe!

Questions? Comments? Just want to say hi? Leave a comment below and let's start a conversation.