Skip to content

Working with Nullability

David Kallesen edited this page Sep 11, 2026 · 1 revision

πŸ•³οΈ Working with Nullability

How to say "this value may be null" in an OpenAPI specification, why the spelling changed between 3.0 and 3.1, and what this generator does with each form.


🧭 The short answer

OpenAPI 3.0 had its own keyword:

email:
  type: string
  nullable: true

OpenAPI 3.1 deleted it and uses the JSON Schema way instead β€” a list of types:

email:
  type: [string, "null"]

Same meaning, different spelling. And one rule that catches almost everyone: nullable: true only works when there is a type right next to it. That rule is the subject of most "nullable isn't working" reports, and the generator now warns about it β€” see Analyzer Rules, rule ATC_API_VER001.


πŸ€” Why it changed

Nobody at Microsoft decided this. The OpenAPI Initiative deleted the keyword from the specification in February 2021. Parsers kept honouring it anyway for another four years, and a 2025 release of Microsoft.OpenApi finally stopped. The rule being enforced is five years older than the release that started enforcing it.

Who changed it

When Who What
Oct 2019 OpenAPI Initiative Publishes the proposal Clarify Semantics of nullable β€” states in writing that nullable applies only when type is in the same Schema Object, and does not reach through allOf / oneOf
Feb 2021 OpenAPI Initiative Releases OpenAPI 3.1.0. nullable is removed from the specification entirely β€” not deprecated, removed
2021–2025 Microsoft.OpenApi Keeps honouring nullable in places the specification never allowed. Lenient, not correct
2025 Microsoft.OpenApi 3.10.x Aligns the parser with the specification. The leniency ends

Why the OpenAPI Initiative removed it

1. It made OpenAPI incompatible with JSON Schema. nullable was never a JSON Schema keyword β€” OpenAPI 3.0 invented it. That meant an OpenAPI schema was not quite a JSON Schema, and the very large ecosystem of JSON Schema validators, editors and code generators could not read OpenAPI documents correctly. OpenAPI 3.1 set out to make the Schema Object a true superset of JSON Schema 2020-12, and a private keyword stood in the way.

2. JSON Schema already had a way to say it. Once 3.1 adopted JSON Schema's type, type: [string, "null"] made nullable pure redundancy.

3. Removed rather than deprecated β€” on purpose. Leaving a non-standard keyword lying around would have defeated the point. A JSON Schema validator meeting nullable: true ignores it silently, and then accepts a null where the author meant to forbid one, or rejects one where the author meant to allow it. A keyword that is quietly wrong is worse than a keyword that is gone.

4. It broke a JSON Schema design rule. JSON Schema keywords are independent: each one only ever adds a constraint, and the result is the intersection of all of them. nullable: true removes a constraint β€” it widens type: string to also permit null. That is why it could never be made to compose sensibly with allOf and friends, and why its semantics needed a whole clarification document in the first place.

πŸ’¬ If someone says "but it worked before": it did, in the same way an unlocked door works until someone checks. The affected properties have been no-ops since OpenAPI 3.0 was published; the generated code was more permissive than the contract the API actually published.


πŸ“ The old format (OpenAPI 3.0)

The rule

From the OpenAPI Initiative's clarification proposal:

"A true value adds "null" to the allowed type specified by the type keyword, only if type is explicitly defined within the same Schema Object."

Two consequences people routinely miss:

  1. No type in the same object β†’ nullable does nothing. Not an error. Just nothing.
  2. It does not reach through allOf / oneOf / anyOf. The specification is explicit: it "does not 'override' or otherwise compete with" schemas brought in by an applicator.

βœ… What works

# scalar
name:
  type: string
  nullable: true

# number
score:
  type: number
  format: double
  nullable: true

# array (the array itself may be null)
tags:
  type: array
  nullable: true
  items:
    type: string

# array whose *items* may be null
tags:
  type: array
  items:
    type: string
    nullable: true

❌ What does not work

# no `type` in this Schema Object - the nullable is a no-op
address:
  nullable: true
  allOf:
    - $ref: '#/components/schemas/Address'

# same problem with oneOf
customer:
  nullable: true
  oneOf:
    - $ref: '#/components/schemas/IdName'

# and a bare $ref cannot be made nullable either;
# sibling keywords next to $ref were ignored in 3.0
address:
  $ref: '#/components/schemas/Address'
  nullable: true

Every one of these is reported as ATC_API_VER001.

πŸ”§ The 3.0 way to make a $ref nullable

Put the nullability inside the composition, as a branch:

address:
  oneOf:
    - $ref: '#/components/schemas/Address'
    - type: object
      nullable: true

It is awkward, and that awkwardness is exactly why 3.1 changed the design. The NexusSample sample specification uses this form for its nullable references.


✨ The new format (OpenAPI 3.1+)

type takes a list. Nullability is just another member of it.

# scalar
name:
  type: [string, "null"]

# number
score:
  type: [number, "null"]
  format: double

# array that may itself be null
tags:
  type: [array, "null"]
  items:
    type: string

# array whose items may be null
tags:
  type: array
  items:
    type: [string, "null"]

# nullable $ref - a null branch in the composition
address:
  oneOf:
    - $ref: '#/components/schemas/Address'
    - type: "null"

πŸ’‘ Quote "null". In YAML, a bare null is the null value, not the string "null". Some parsers cope; not all do. type: [string, "null"] is unambiguous.

Also worth knowing in 3.1

$ref gained the ability to carry sibling keywords, so this is legal in 3.1 where it was ignored in 3.0:

address:
  $ref: '#/components/schemas/Address'
  description: Where the invoice goes

Nullability still belongs in a oneOf branch rather than as a $ref sibling.


πŸ“Š Side by side

Intent OpenAPI 3.0 OpenAPI 3.1+
Nullable string type: string
nullable: true
type: [string, "null"]
Nullable integer type: integer
nullable: true
type: [integer, "null"]
Nullable enum type: string
enum: [a, b]
nullable: true
type: [string, "null"]
enum: [a, b, null]
Nullable array type: array
nullable: true
type: [array, "null"]
Nullable array items items:
type: string
nullable: true
items:
type: [string, "null"]
Nullable $ref oneOf:
- $ref: ...
- type: object
nullable: true
oneOf:
- $ref: ...
- type: "null"

⚠️ The enum row has a trap. In 3.1, adding "null" to type is not enough on its own β€” null also has to be a permitted enum value, because enum is an independent constraint. This is the keyword-independence principle in action.


πŸ› οΈ What this generator accepts

Both spellings, wherever the information survives parsing:

Spelling 3.0 3.1 3.2 Note
type: X + nullable: true βœ… βœ… βœ… 3.1+ is a deliberate leniency β€” the keyword is invalid there, but honouring it avoids silently changing your models
type: [X, "null"] n/a βœ… βœ… Type arrays do not exist in 3.0
nullable: true + allOf/oneOf, no type ❌ βœ… βœ… See below
oneOf: [$ref, {type: "null"}] n/a βœ… βœ… The idiomatic 3.1 nullable reference
oneOf: [$ref, {type: object, nullable: true}] βœ… βœ… βœ… The 3.0 nullable reference - 3.0 has no type: "null" to write

The one case that cannot work

nullable: true with no type, in a 3.0 document.

In a 3.0 document the parser recognises nullable, consumes it, and then discards it because there is no type in that Schema Object for it to modify. Nothing reaches the generator β€” not the type flag, not UnrecognizedKeywords, not Extensions. There is nothing left to act on, which is why the ATC_API_VER001 rule reads the raw specification text rather than the parsed document.

In a 3.1 document the same text is an unknown keyword, so the parser parks it in UnrecognizedKeywords instead of consuming it β€” which is why the generator can still honour it there. A quirk of how the two spec versions are parsed, not a design choice.

Per the specification, those properties were never nullable in the first place. Older parser versions were simply lenient about it.


πŸ”Ž How to audit a specification

Build with "validateSpecificationStrategy": "Strict" and the generator reports each one with a line number and a suggested rewrite. To do it by hand, find every nullable: true and check whether a type: sits at the same indentation, in the same mapping block:

  • Has a sibling type: β†’ valid 3.0, works, nothing to do.
  • No sibling type: β†’ a no-op. Decide whether the property is genuinely nullable:
    • Yes β†’ rewrite using the oneOf form for your spec version.
    • No β†’ delete the nullable: true. It was never doing anything.

That second branch matters more than it looks. These declarations have been inert for as long as the specification has existed, so the API may simply never return null there β€” in which case translating them would widen a contract that was already correct.


🚨 Related analyzer rules

Four rules cover the constructs whose meaning depends on the declared version. All of them run under the Strict validation strategy; see Analyzer Rules for the full descriptions.

Rule Fires on What it catches
ATC_API_VER001 3.0 documents nullable with no sibling type β€” inert, and the property is not generated as nullable
ATC_API_VER002 3.1+ documents The nullable keyword at a version that removed it
ATC_API_VER003 3.1+ documents Boolean exclusiveMinimum / exclusiveMaximum, which 3.1 replaced with a numeric bound
ATC_API_VER004 3.0 documents A type array, which 3.0 does not allow

The design is that a correct document is silent at every version. A 3.0 document only hears about declarations that are inert today; the migration advice arrives once you actually raise openapi:, which is the point at which it becomes actionable.


πŸ“š Further reading

🏠 Home

πŸ’Ό Why This Tool?

πŸ“– Getting Started

βš™οΈ Features

🌐 Frontend

πŸ“‹ Reference


πŸ”— Resources

Clone this wiki locally