-
Notifications
You must be signed in to change notification settings - Fork 1
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.
OpenAPI 3.0 had its own keyword:
email:
type: string
nullable: trueOpenAPI 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.
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.OpenApifinally stopped. The rule being enforced is five years older than the release that started enforcing 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 |
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.
From the OpenAPI Initiative's clarification proposal:
"A
truevalue adds"null"to the allowed type specified by thetypekeyword, only iftypeis explicitly defined within the same Schema Object."
Two consequences people routinely miss:
-
No
typein the same object βnullabledoes nothing. Not an error. Just nothing. -
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.
# 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# 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: trueEvery one of these is reported as ATC_API_VER001.
Put the nullability inside the composition, as a branch:
address:
oneOf:
- $ref: '#/components/schemas/Address'
- type: object
nullable: trueIt is awkward, and that awkwardness is exactly why 3.1 changed the design. The NexusSample sample
specification uses this form for its nullable references.
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 barenullis the null value, not the string"null". Some parsers cope; not all do.type: [string, "null"]is unambiguous.
$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 goesNullability still belongs in a oneOf branch rather than as a $ref sibling.
| Intent | OpenAPI 3.0 | OpenAPI 3.1+ |
|---|---|---|
| Nullable string |
type: stringnullable: true
|
type: [string, "null"] |
| Nullable integer |
type: integernullable: true
|
type: [integer, "null"] |
| Nullable enum |
type: stringenum: [a, b]nullable: true
|
type: [string, "null"]enum: [a, b, null]
|
| Nullable array |
type: arraynullable: 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"totypeis not enough on its own βnullalso has to be a permittedenumvalue, becauseenumis an independent constraint. This is the keyword-independence principle in action.
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 |
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.
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
oneOfform for your spec version. -
No β delete the
nullable: true. It was never doing anything.
-
Yes β rewrite using the
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.
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.
- Upgrading to v2 β what changed in this generator, and whether your specifications are affected.
- Working with OpenAPI β supported versions and features.
-
Analyzer Rules β every diagnostic, including the
VERrules above. - Migrating from OpenAPI 3.0 to 3.1.0 β OpenAPI Initiative
- Clarify Semantics of nullable β OAI proposal (2019)
-
Upgrading from OpenAPI 3.0 to 3.1 β learn.openapis.org β note it does not cover the
$ref/allOfcase, which is part of why that mistake is so widespread - OpenAPI Specification 3.1.0 β Schema Object
π Home
- πΌ FAQ Business Value
- π Getting Started with Basic
- π οΈ Getting Started with CLI
- π Migration Guide
- β¬οΈ Upgrading to v2
- π Working with OpenAPI
- π³οΈ Working with Nullability
- π οΈ Working with CLI
- π How-To Guides
- π Working with Security
- π¦ Working with Rate Limiting
- π Working with Resilience
- ποΈ Working with Caching
- π’ Working with Versioning
- β Working with Validations
- π Working with Webhooks
- βοΈ Working with Aspire
- π£οΈ Working with Endpoint Definitions
- π Working with Multi-Part Specs
- π§ͺ Working with Code Coverage
- π Working with C# Client
- π§ͺ Working with C# Client Testing
- π¦ Working with TypeScript Client
- πͺ Showcase Demo
- π§ͺ Working with E2E Testing
- βοΈ Working with Configuration
- π Marker Files
- π API Reference
- π Analyzer Rules
- β FAQ and Troubleshooting
- πΊοΈ Roadmap
- π§ Development Notes
- π¦ GitHub Repository
- π₯ NuGet Package
- π Report Issues