Skip to content

OpenAPI 3.1 V31 deserializer loses nullable when "nullable" key appears before "type" (key-order sensitive) #2845

Description

@jrabau

[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:

  1. Load the schema above with OpenApiDocument.LoadAsync
  2. Inspect components.schemas.MyObject.properties.system_fingerprint
  3. 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

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions