[Bug] OpenAPI 3.1 V31 deserializer loses nullable when "nullable" key appears before "type" (key-order sensitive)
Describe the bug
When parsing an OpenAPI 3.1 spec, the V31 deserializer silently loses nullability for properties that use nullable: true if the nullable key appears before type in the JSON object. The result is that nullable: true is ignored and the property is treated as non-nullable.
This is a key-ordering bug: the "nullable" handler sets o.Type = JsonSchemaType.Null when o.Type is not yet defined, but the "type" handler then does a direct assignment o.Type = parsedType, overwriting the Null flag. If the keys appear in the opposite order (type before nullable), the handler correctly does o.Type |= JsonSchemaType.Null and the nullability is preserved.
This does not affect OpenAPI 3.0 specs the V30 deserializer handles nullable as a post-processing step after all keys are read, making it order-independent.
Note on spec validity: nullable: true is technically an OpenAPI 3.0 construct and does not exist in the OpenAPI 3.1 / JSON Schema 2020-12 vocabulary (where nullability is expressed as "type": ["string", "null"]). However, since the library explicitly supports nullable in 3.1 for backwards compatibility, it should do so consistently regardless of key ordering.
OpenApi File To Reproduce
Minimal reproducer (note: nullable appears before type):
{
"openapi": "3.1.0",
"info": { "title": "Test", "version": "1.0" },
"components": {
"schemas": {
"MyObject": {
"type": "object",
"required": ["system_fingerprint"],
"properties": {
"system_fingerprint": {
"description": "System fingerprint",
"nullable": true,
"type": "string"
}
}
}
}
}
}
Steps to reproduce:
- Load the schema above with
OpenApiDocument.LoadAsync
- Inspect
components.schemas.MyObject.properties.system_fingerprint
- Observe that
Schema.Type is String (16) and does not include Null
Expected behavior
system_fingerprint should be parsed as nullable Schema.Type should be Null | String (17) regardless of whether nullable or type appears first in the JSON object.
Screenshots / Code Snippets
Tested on Microsoft.OpenApi 3.5.3. Results:
| Key order |
OpenAPI version |
Schema.Type |
Nullable |
nullable before type |
3.1 |
String (16) |
❌ |
type before nullable |
3.1 |
Null | String (17) |
✅ |
nullable before type |
3.0 |
Null | String (17) |
✅ |
type before nullable |
3.0 |
Null | String (17) |
✅ |
The root cause is in OpenApiV31Deserializer. The "type" handler overwrites o.Type directly without preserving an existing Null flag set by a preceding "nullable": true:
// Current behavior — overwrites any previously set Null flag
"type", (o, n, _) => o.Type = n.GetScalarValue()?.ToJsonSchemaType()
A minimal fix would be to preserve the Null flag if already set:
"type", (o, n, _) =>
{
var parsed = n.GetScalarValue()?.ToJsonSchemaType();
o.Type = (o.Type.HasValue && o.Type.Value.HasFlag(JsonSchemaType.Null))
? parsed | JsonSchemaType.Null
: parsed;
}
Additional context
- Affects Microsoft.OpenApi up to and including 3.5.3 (v3.5.3 only addresses boolean schema handling)
- The V30 deserializer is not affected because it stores
nullable as a deferred extension and applies it after all keys are parsed
- As a workaround, pre-normalizing the JSON document by rewriting
{ "nullable": true, "type": "X" } to { "type": ["X", "null"] } before calling LoadAsync produces the correct result
AI Assisted
[Bug] OpenAPI 3.1 V31 deserializer loses nullable when "nullable" key appears before "type" (key-order sensitive)
Describe the bug
When parsing an OpenAPI 3.1 spec, the V31 deserializer silently loses nullability for properties that use
nullable: trueif thenullablekey appears beforetypein the JSON object. The result is thatnullable: trueis ignored and the property is treated as non-nullable.This is a key-ordering bug: the
"nullable"handler setso.Type = JsonSchemaType.Nullwheno.Typeis not yet defined, but the"type"handler then does a direct assignmento.Type = parsedType, overwriting theNullflag. If the keys appear in the opposite order (typebeforenullable), the handler correctly doeso.Type |= JsonSchemaType.Nulland the nullability is preserved.This does not affect OpenAPI 3.0 specs the V30 deserializer handles
nullableas a post-processing step after all keys are read, making it order-independent.OpenApi File To Reproduce
Minimal reproducer (note:
nullableappears beforetype):{ "openapi": "3.1.0", "info": { "title": "Test", "version": "1.0" }, "components": { "schemas": { "MyObject": { "type": "object", "required": ["system_fingerprint"], "properties": { "system_fingerprint": { "description": "System fingerprint", "nullable": true, "type": "string" } } } } } }Steps to reproduce:
OpenApiDocument.LoadAsynccomponents.schemas.MyObject.properties.system_fingerprintSchema.TypeisString(16) and does not includeNullExpected behavior
system_fingerprintshould be parsed as nullableSchema.Typeshould beNull | String(17) regardless of whethernullableortypeappears first in the JSON object.Screenshots / Code Snippets
Tested on Microsoft.OpenApi 3.5.3. Results:
Schema.TypenullablebeforetypeString(16)typebeforenullableNull | String(17)nullablebeforetypeNull | String(17)typebeforenullableNull | String(17)The root cause is in
OpenApiV31Deserializer. The"type"handler overwriteso.Typedirectly without preserving an existingNullflag set by a preceding"nullable": true:A minimal fix would be to preserve the
Nullflag if already set:Additional context
nullableas a deferred extension and applies it after all keys are parsed{ "nullable": true, "type": "X" }to{ "type": ["X", "null"] }before callingLoadAsyncproduces the correct resultAI Assisted