Deserializzazione JSON sicura in .NET 10: guida completa a JsonSerializerOptions.Strict
Considera questo payload JSON in arrivo alla tua API:
{"Amount": 100, "Amount": -999}
Due proprietà con lo stesso nome. La sezione 4 di RFC 8259 dice che i nomi degli oggetti dovrebbero essere univoci, ma non lo impone. System.Text.Json, di default, adotta l’approccio permissivo: vince l’ultima scrittura, nessun avviso, nessun errore. Il valore dell’attaccante passa silenziosamente.Questo non è solo un problema di proprietà duplicate. La deserializzazione di default ignora anche campi extra che un attaccante potrebbe iniettare, lascia scivolare i valori null nelle proprietà non-nullable e salta dati richiesti mancanti. Ogni di queste “comodità” è una potenziale vulnerabilità al confine della tua API.
JsonSerializerOptions.Strict: cinque protezioni in un solo preset
.NET 10 introduce JsonSerializerOptions.Strict, un nuovo preset di sola lettura che si affianca a Default e Web. Mentre Default dà priorità alla retrocompatibilità e Web ottimizza per le API HTTP tipiche, Strict segue le best practice di sicurezza attivando cinque impostazioni protettive simultaneamente.
var strict = JsonSerializerOptions.Strict;
// AllowDuplicateProperties: False
// UnmappedMemberHandling: Disallow
// PropertyNameCaseInsensitive: False
// RespectNullableAnnotations: True
// RespectRequiredConstructorParameters: True
Confronto tra i tre preset
| Impostazione | Default | Web | Strict |
|---|
| AllowDuplicateProperties | true | true | false |
| UnmappedMemberHandling | Skip | Skip | Disallow |
| PropertyNameCaseInsensitive | false | true | false |
| RespectNullableAnnotations | false | false | true |
| RespectRequiredConstructorParameters | false | false | true |
I dati serializzati con Default possono essere deserializzati con Strict. La compatibilità va in una sola direzione: Strict è più severo su ciò che accetta, non su ciò che produce.
1. Proprietà duplicate vietate
I protocolli che stratificano il parsing JSON (OAuth 2.0, OpenID Connect, firme webhook) possono essere sfruttati se parser diversi gestiscono input duplicati in modo diverso. Con Strict, ogni tentativo di deserializzare JSON con proprietà duplicate genera immediatamente una JsonException:
string duplicateJson = @'{"Amount": 100, "Amount": -999}';
try
{
JsonSerializer.Deserialize<Payment>(duplicateJson, JsonSerializerOptions.Strict);
}
catch (JsonException ex)
{
// JsonException: Duplicate property 'Amount' encountered during deserialization
Console.WriteLine(ex.Message);
}
public record Payment(int Amount);
Questa protezione si estende oltre i POCO (plain-old C# objects): funziona anche con JsonDocument, JsonNode e Dictionary<string, T>.
2. Rifiuto dei membri non mappati
La deserializzazione di default scarta silenziosamente le proprietà JSON che non corrispondono al tuo tipo .NET. È comodo durante lo sviluppo, ma è pericoloso a un confine di fiducia perché non sai cosa sta inviando il client.
string extraFieldJson = @'{"Name": "Alice", "Role": "user", "IsRoot": true}';
// Default: ignora silenziosamente "IsRoot"
var user = JsonSerializer.Deserialize<User>(extraFieldJson);
// Name=Alice, Role=user - "IsRoot" scompare senza tracce
// Strict: rifiuta la proprieta' non mappata
JsonSerializer.Deserialize<User>(extraFieldJson, JsonSerializerOptions.Strict);
// throws: The JSON property 'IsRoot' could not be mapped to any .NET member
public record User(string Name, string Role);
3. Corrispondenza case-sensitive dei nomi di proprietà
In modalità Strict, la case sensitivity diventa un contratto preciso: i nomi delle proprietà JSON devono corrispondere esattamente ai nomi delle proprietà C#. Se i tuoi client inviano camelCase ma i tuoi tipi usano PascalCase, aggiungi [JsonPropertyName("nomeCamelCase")] per rendere il contratto esplicito nella definizione del tipo.
4. Enforcement delle annotazioni nullable
I nullable reference types di C# aiutano a intercettare i problemi di null a compile time, ma System.Text.Json li ignora di default durante la deserializzazione. Con Strict, se hai dichiarato string Name (non string? Name), il serializzatore rifiuterà qualsiasi JSON con null per quella proprietà:
string nullNameJson = @'{"Name": null, "Email": "alice@example.com"}';
// Default: null va nella stringa non-nullable senza errori
var contact = JsonSerializer.Deserialize<Contact>(nullNameJson);
// contact.Name == null (silenzioso!)
// Strict: genera eccezione
JsonSerializer.Deserialize<Contact>(nullNameJson, JsonSerializerOptions.Strict);
// throws: The constructor parameter 'Name' doesn't allow null values
public record Contact(string Name, string Email);
5. Parametri obbligatori del costruttore
I record type e le classi con costruttori parametrizzati possono avere parametri obbligatori silenziosamente riempiti con valori di default quando il JSON manca dei dati. Strict lo impedisce:
string missingParamJson = @'{"FirstName": "Alice"}';
// Default: LastName mancante diventa silenziosamente null
var person = JsonSerializer.Deserialize<Person>(missingParamJson);
// person.LastName == null
// Strict: richiede tutti i parametri
JsonSerializer.Deserialize<Person>(missingParamJson, JsonSerializerOptions.Strict);
// throws: JSON deserialization was missing required properties: 'LastName'
public record Person(string FirstName, string LastName);
Integrazione in ASP.NET Core Minimal APIs
Nei demo sopra usiamo JsonSerializer direttamente. In un’applicazione web, configuri le opzioni JSON una volta e ogni endpoint le eredita. Nota: JsonSerializerOptions.Strict è un singleton frozen, quindi non puoi passarlo direttamente a ConfigureHttpJsonOptions che richiede un’istanza mutabile. Imposta le singole proprietà:
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;
});
app.MapPost("/payments", (Payment payment) =>
{
// Se il body ha proprieta' duplicate, campi non mappati o dati mancanti,
// il framework risponde con 400 Bad Request prima che questo codice venga eseguito.
return Results.Ok(payment);
});
Il framework intercetta JsonException durante il model binding e restituisce un 400 Bad Request con problem details. Il tuo endpoint vede solo oggetti validi e completamente inizializzati.
Configurazione per-endpoint
Se hai bisogno di validazione strict su alcuni endpoint ma parsing più flessibile su altri, puoi deserializzare manualmente dal body della richiesta con le opzioni desiderate:
app.MapPost("/api/strict", async (HttpContext context) =>
{
var payment = await context.Request.ReadFromJsonAsync<Payment>(
JsonSerializerOptions.Strict);
return Results.Ok(payment);
});
Supporto per i Source Generator
Per scenari AOT o per i benefici prestazionali dei source generator, configura manualmente le impostazioni equivalenti su JsonSourceGenerationOptionsAttribute. Non esiste una scorciatoia Strict per l’attributo: ogni proprietà va impostata individualmente.
[JsonSourceGenerationOptions(
AllowDuplicateProperties = false,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
PropertyNameCaseInsensitive = false,
RespectNullableAnnotations = true,
RespectRequiredConstructorParameters = true
)]
[JsonSerializable(typeof(Payment))]
internal partial class StrictJsonContext : JsonSerializerContext;
Il codice generato include tutta la logica di validazione a compile time, senza overhead di reflection.
Quando usare Strict (e quando no)
Usalo ai confini di fiducia: endpoint token, ricevitori di webhook, controller API che accettano JSON da client non controllati completamente. Il costo è una JsonException quando i payload non corrispondono al contratto. Questo è esattamente lo scopo.
Evitalo per l’ingestione flessibile: se consumi JSON da API di terze parti con schemi inconsistenti, la modalità strict rifiuterà payload che potresti voler gestire con più grazia. In questi casi usa Default o Web e valida dopo la deserializzazione.
Migra in modo incrementale: non è necessario passare tutto a Strict subito. Inizia dagli endpoint ad alto rischio, intercetta JsonException, registra i problemi, correggi i client che inviano payload non conformi, poi espandi.
Sappi i limiti: Strict valida le violazioni del contratto strutturale ma non protegge da JSON profondamente annidato (usa MaxDepth), payload eccessivi (imposta limiti HTTP) o type confusion polimorfico. È un layer di difesa, non l’unico.
Conclusione
Ogni endpoint API che accetta JSON è un confine di fiducia. La deserializzazione permissiva rende quel confine poroso. JsonSerializerOptions.Strict non aggiunge nuova logica: attiva protezioni già presenti in System.Text.Json ma disattivate di default per retrocompatibilità. Una riga di configurazione le attiva tutte.
Questo è particolarmente rilevante ai confini di protocollo come OAuth 2.0 e OpenID Connect, dove una proprietà duplicata o un campo inatteso non è solo un bug — è un potenziale vettore di exploit.
Fonte: Harden Your .NET JSON Deserialization with System.Text.Json and JsonSerializerOptions.Strict — Khalid Abuhakmeh, Duende Software (30 aprile 2026)
Learn how .NET 10's new JsonSerializerOptions.Strict preset activates five security protections in System.Text.Json to close gaps at your API trust boundaries.
Khalid Abuhakmeh (Duende Software)
simone
in reply to Danilo ® • • •Gianfranco
in reply to Danilo ® • • •Penso che l'AI possa far solo bene all'open source.
E' il momento di continuare a sviluppare, ottimizzare e costruire insieme protocolli e software open source sfruttando milioni di agenti AI.
Danilo ®
2026-04-30 07:36:13