Skip to content

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

@jrabau

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

AI Assisted

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions