# OpenAPI 3: One Nullable Keyword Deleted, Three Encodings Win

Brady Weaver · August 24, 2026

> OpenAPI 3: One Nullable Keyword Deleted, Three Encodings Win. Twenty-seven months. That is how long the most widely deployed API docu...

| Takeaway | Detail |
| --- | --- |
| Nullability loss validates cleanly — that silence is the defect. | Under OpenAPI 3.1's JSON Schema alignment, the retired `nullable` keyword degrades to an ignorable annotation: a schema whose type array lost its "null" entry reports zero errors from a bare-bones linter or a $500 governance suite alike, and the stripped union resurfaces only as production deserialization crashes months later, never as lint output. |
| Ecosystem tooling lagged the spec by years, not weeks. | OpenAPI 3.1.0 shipped with type arrays as its single new nullability syntax, yet the most widely deployed documentation viewer could not render compliant documents for the next 27 months — so even correctly migrated schemas looked broken in the primary human review surface. |
| Automated migration assistants are necessary, not sufficient. | Atlassian's Jira Cloud pre-migration checklist (published December 16, 2020) runs alongside its Migration Assistant precisely because the tool "won't check for everything"; its numbered table marks every action Mandatory, beginning with creating a migration plan — a pattern worth replicating as per-schema deserialization smoke tests. |
| Pin the exact build where behavior changed, or the regression cannot be bisected. | Bifrost's v2.0.0 upgrade guide isolates exactly four breaking changes behind a What changed / Who is affected / How to fix it structure and pins the shift to prerelease build 2.0.0-prerelease3 — the traceability that silent nullability stripping lacks, because nothing fails at migration time to pin. |

Twenty-seven months. That is how long the most widely deployed API documentation viewer failed to render documents conforming to OpenAPI 3.1.0 after the specification shipped — a compatibility gap sitting directly on top of the version's single new mechanism for declaring nullability.

The danger is not that the old `nullable` keyword breaks anything. Under 3.1's alignment with JSON Schema, a removed keyword behaves as an ignorable annotation: validators read past it, generators read past it, and a schema whose type array quietly lost its "null" entry still passes every gate. Whether the tooling budget runs to a bare-bones linter or a $500 enterprise governance suite, the output is identical — zero errors — while generated SDKs strip every null union and surface months later as production deserialization crashes.

Migrations this quiet defeat playbooks built for loud breakers. The strongest available templates come from adjacent versioned-API work: Bifrost's v2.0.0 guide isolates exactly four breaking changes behind a What changed / Who is affected / How to fix it frame, and Atlassian's Jira Cloud checklist pairs every action with a Mandatory designation precisely because its automated assistant "won't check for everything." Nullability deserves the same treatment — with silence itself treated as the failure mode.

![Three parallel glass walkways converge inside vast minimalist](https://static.mm-ais.com/article-images-ai/openapi-3-one-nullable-keyword-deleted-t-ai-c81726bf.jpg)
Three parallel glass walkways converge inside vast minimalist

## One Keyword Deleted

The OpenAPI Initiative — a Linux Foundation project — published version 3.1.0 on a new foundation: the JSON Schema dialect submitted to the IETF as draft-bhutton-json-schema-00. That adoption decision is the entire origin story of the nullability problem. Once a JSON Schema dialect becomes the base, schema objects stop speaking OpenAPI vocabulary and start speaking JSON Schema vocabulary — which is precisely why nullability stopped being an OpenAPI-specific keyword and became a property of the shared type keyword. Every consequence described below descends from that single alignment.

The deletion itself is mechanically trivial and semantically treacherous. OpenAPI 3.0 defined nullable as a boolean sibling of type; 3.1 removes it from the specification entirely. Under JSON Schema's core semantics, an unrecognized keyword is an annotation, not a constraint — so a lenient parser that encounters nullable: true inside a 3.1.0 document raises no error. It files the keyword away as metadata and quietly discards the information it carried. That is why the breakage is silent instead of loud: the most dangerous line in a migrated document is one that parses cleanly and means nothing. If your documentation portal renders nullable: true without complaint, that is evidence the parser declined to enforce anything — not evidence the document is valid.

The replacement is a grammar extension, not a new keyword. The adopted draft defines type to accept either a single string or an array of strings, and the literal "null" is a legal array member. type: ["string", "null"] therefore expresses, in one standard construct, exactly what 3.0 needed a vendor keyword to express — written in vocabulary every JSON Schema validator aligned with that dialect already understands.

The result is a toolchain that disagrees with itself about the same file. Strict validators flag or reject nullable inside a document declared openapi: 3.1.0, because the keyword no longer exists in that dialect; lenient viewers and parsers display the identical file without comment. One document, two opposite verdicts — and the lenient verdict is usually the first one a developer sees in the authoring workflow.

| Tool class | Named examples | Verdict on nullable: true in openapi: 3.1.0 |
| --- | --- | --- |
| Strict validators and linters | Spectral (oas3_1 ruleset), Redocly CLI lint | Flag or reject the document |
| Lenient viewers and parsers | Documentation portals, preview renderers | Render normally; discard the keyword as an annotation |

Treat "it renders" as a null signal. Where the deleted keyword does real damage is one hop further down the chain, in generated SDKs: code generators translate the two-element type array into native null unions, while a discarded nullable keyword produces signatures with no null member at all.

| Language | Signature from type: ["string", "null"] | Signature after nullable is discarded | First failure |
| --- | --- | --- | --- |
| TypeScript | string \| null | string | Runtime type error on the first null payload |
| Kotlin | String? | String | Deserialization error on the first null field |
| Python | Optional[str] | str | Type-checker or validator failure on None |

Quantify the delta and the stakes sharpen: between 3.0.3 and 3.1.0, the nullability model changed by exactly one deleted keyword and one grammar extension — the smallest breaking change in the migration — yet it touches nearly every schema in a typical API description, because most descriptions carry at least one optional property. Minimal spec surface, maximal document coverage, invisible failure: by any structured-authoring standard, that profile is the critical path. Gate each merge on the two checks a lenient chain cannot fake — zero occurrences of nullable anywhere in the 3.1.0 source, and a regenerated SDK diff that visibly shows the null union on every signature that previously claimed nullable: true.

![Three gravel footpaths diverge across windswept coastal moor](https://static.mm-ais.com/article-images-ai/openapi-3-one-nullable-keyword-deleted-t-ai-96c84355.jpg)
Three gravel footpaths diverge across windswept coastal moor

## The 27-Month Convergence

The convergence on OpenAPI 3.1 was sequenced by tooling layer, not by publication date. According to the springdoc-openapi 2.0.0 release notes (November 2022), Java teams began emitting 3.1.0 documents as the default output for Spring Boot 3 applications — before several of their own downstream consumers could parse them. That inversion set the pattern for the following year: producers moved first, consumers trailed, and one tier has still not moved at all.

FastAPI followed the same delayed path, for a structural reason worth internalizing. According to FastAPI's 0.99.0 release notes, the framework's default generated document flipped from 3.0.3 to 3.1.0 — but only because Pydantic v2 exposed a JSON-Schema-aligned emission mode underneath it. Server-side spec producers adopted 3.1 more than two years after publication because the schema engine beneath the web framework had to move first. Adoption was a dependency-chain event, not a decision.

The SDK side lagged worst, and it failed in the more dangerous direction. In the OpenAPITools/openapi-generator tracker, the 6.x line either threw compile errors on ["string", "null"] or — worse — silently stripped the union down to plain string across multiple language templates. Version 7.0.0 closed the largest of those gaps, putting generator convergence well after the spec itself had shipped. Silent stripping is the failure mode that matters: continuous integration stays green, the generated type quietly loses its nullability, and the defect surfaces at runtime on the first real null the API sends.

Even the reference documentation viewer was out of step. According to the Swagger UI 5.0 release notes, that was the first major release with 3.1 rendering support — approximately 27 months after that publication. Readers consulting the canonical viewer during that window absorbed a picture of nullability shaped by a parser older than the spec it displayed. Retire the oldest excuse in this migration accordingly: "nullable: true still works — our docs portal renders it fine." During exactly this period, the documents rendering incorrectly were the valid ones using type arrays. Rendering tracks your viewer's vintage, not your schema's validity — lenient parsers ignore nullable as an annotation, strict validators reject the document outright, and generators that ignore it hand you non-nullable SDK types.

Two stragglers bound the 2026 edge cases. Gateway tier: according to AWS's current API Gateway import/export documentation, OpenAPI 3.0 remains the supported definition version — a hard ceiling for architectures that push definitions directly to the gateway. Middleware: getkin/kin-openapi, the de facto Go validator behind many API gateways and mock servers, merged 3.1 request/response validation in later releases, so Go-based enforcement pipelines could not validate 3.1 contracts until two years after publication. Keep the authored source in 3.1 with type arrays and treat any 3.0 gateway export as a generated build artifact — downconversion is precisely where nullable re-enters otherwise clean documents.

The merge gate follows directly. Before approving any migration merge, search the 3.1 document for the literal string nullable — under the guide's blocking rule, any hit stops the merge, whether it is a legacy spelling or a downconversion artifact. Then inspect the regenerated SDK diff and confirm the null union appears in every previously optional field. A diff showing bare types where unions belong means the generator, not the document, is lying to you.

| Ecosystem layer | Milestone | Date | Convergence signal | Status in 2026 |
| --- | --- | --- | --- | --- |
| Java producer | springdoc-openapi 2.0.0 | November 2022 | 3.1.0 became default output for Spring Boot 3 | Converged |
| Python producer | FastAPI 0.99.0 on Pydantic v2 | After Pydantic v2 | Default document flipped from 3.0.3 to 3.1.0 | Converged |
| Renderer | Swagger UI 5.0 | Post-publication | First major release with 3.1 rendering support | Converged |
| SDK generator | openapi-generator 7.0.0 | Post-publication | Closed largest type-array stripping and compile-error gaps | Converged |
| Go middleware | getkin/kin-openapi | Post-publication | 3.1 request/response validation merged | Converged |
| Gateway tier | AWS API Gateway import/export | Current AWS docs | Still specifies OpenAPI 3.0 as supported version | Hard ceiling |

![The 27-Month Convergence — OpenAPI 3](https://static.mm-ais.com/article-images-pixabay/openapi-3-one-nullable-keyword-deleted-t-ea7cca7b.jpg)

## Three Spellings, One Winner

Three encodings can mark a string nullable in an OpenAPI 3.1 document. Two parse cleanly almost everywhere; one survives generation intact. Score each candidate on the four axes that decide the merge — spec validity under 3.1.0, strict-validator outcome, generator union fidelity, authoring cost — before touching the spec file.

Encoding A, `type: ["string", "null"]`, is the explicit winner for every plain property. It is the form the adopted dialect was built for, so a generator faces exactly one decision per property, and current chains make it correctly: TypeScript clients receive `string | null`, Kotlin clients `String?`, and Python models built with datamodel-code-generator receive `Optional[str]`. Structured authoring adds a second reason: the whole constraint sits on one reviewable line, so a regenerated SDK diff exposes the null union immediately — the exact artifact the migration merge gate inspects.

Encoding B, `anyOf: [{type: string}, {type: "null"}]`, is semantically identical — both branches describe the same two-case type — but it roughly triples the markup per property, since each branch becomes its own braced object and reviewers read three lines where one sufficed. It also carries a fidelity tax: some generators flatten the anyOf into a looser union than the document specifies, narrowing the null out of the SDK even though the document validates. Reserve B for the two cases a type array physically cannot cover — the nullable member is a `$ref` (elements of `type` must be strings, never references), or a branch must carry sibling keywords such as `format` beside the null case.

Encoding C, the legacy `nullable: true` sibling, is the trap the docs portal makes comfortable. Under the adopted dialect an unrecognized keyword imposes no validation effect, so lenient parsers file it as inert annotation: the portal renders, the linter shrugs. Strict pipelines do the opposite — they reject the document or strip the keyword before generation, and the emitted SDK types arrive non-nullable. The crash surfaces far from the authoring repo, on the first real null the API sends. Rendering is not validity; disqualify C for all new 3.1 authoring, with no exception.

| Encoding | Valid under 3.1.0? | Strict validator | Generator union fidelity | Authoring cost |
| --- | --- | --- | --- | --- |
| A — `type: ["string", "null"]` | Yes; native dialect form | Passes strict rulesets (Spectral, Redocly CLI) | Clean union: `string \| null`, `String?`, `Optional[str]` | One array wrapper |
| B — `anyOf: [{type: string}, {type: "null"}]` | Yes; semantically equivalent | Passes | Some generators flatten the anyOf into a looser union | Roughly triples markup per property |
| C — `nullable: true` sibling | No; keyword removed from the 3.1 vocabulary | Rejects the document | Lenient parsers drop it; SDK types emerge non-nullable | Looks free; repaid at the first generated null |

The table implies one legitimate exit. If your only downstream consumer is pinned to 3.0 — a gateway importer that rejects 3.1 outright — the correct decision is to remain on 3.0.3 end-to-end and keep `nullable: true`, which is valid in that vocabulary, rather than half-migrate into a 3.1 document carrying 3.0 habits. The type-array rule governs documents whose full pipeline you control; a pinned importer means you do not control it yet, so pin the document version to the pipeline you have.

The verdict compresses to one sentence: for any property whose null must survive into generated SDKs, type arrays win outright; anyOf is a constrained-composition fallback; the legacy keyword has zero winning scenarios in a 3.1 document.

![deleted removed archive only](https://static.mm-ais.com/article-images-pixabay/openapi-3-one-nullable-keyword-deleted-t-dd5ebc1f.jpg)
deleted removed archive only

## What the Data Doesn't Tell You

The convergence record proves less than it appears to. Every data point behind it comes from release notes, changelogs, and conformance suites — artifacts written by the projects that converged, about the versions they shipped. That corpus carries a survivorship bias no amount of rereading corrects: a team whose generator crashed on the new syntax files an issue, and the failure enters the record. A team whose generator compiles cleanly but silently drops the null union files nothing, because nothing broke — yet. Silent failures generate no changelog entries, no issue threads, no conference talks. The absence of complaints in the record is not evidence of absence in the wild.

Conformance suites widen the gap. They exercise small, well-formed documents — a property, a type array, an expected pass. Production specifications layer allOf composition, discriminator mappings, $ref targets, and vendor extensions (x-) on top, and parser behavior under that stacking is precisely what published tests rarely cover. A generator can handle type: ["string", "null"] in isolation and still mishandle the same union reached through a $ref, because nobody wrote that test.

Variance across cases is structural, not noise. The same conforming document yields different SDKs depending on which language template consumes it: mature Java and TypeScript templates map the union faithfully, while several smaller templates in openapi-generator flatten a two-element type array back to the bare base type — emitting a non-nullable field from a nullable spec. Validation gates diverge too. Strict validators reject any document still carrying the deleted keyword; lenient ones accept it as an inert annotation. "It passes" is therefore a statement about your pipeline, not your document.

This is the mechanism behind the most seductive myth in migration threads: our docs portal renders nullable: true fine, so it still works. Rendering is not validity. A lenient parser ignores the unknown keyword as an annotation; a strict validator rejects the document outright; and a generator that ignores it emits a non-nullable SDK type that crashes on the first real null the API sends. The portal proves nothing because it sits upstream of every component that matters.

So when does the rule break? Never on validity — the type array remains the only correct encoding in a 3.1 document, full stop. It breaks operationally, in three recurring situations. First, dual-dialect publishing: consumers still pinned to the prior major version cannot parse type arrays at all, so the fix is a separately converted artifact, not one document serving both dialects. Second, custom codegen templates: any Mustache branch keyed on the deleted keyword now matches nothing, and the defect surfaces only in a regenerated-SDK diff. Third, framework-emitted specifications: hand-editing the artifact buys one clean lint run before the next regeneration overwrites it — the correction belongs in the emitter's configuration. Read the matrix below as the audit list those situations imply. In every row the encoding wins; what varies is where you point the verification.

| Edge case | What the record cannot tell you | Verify before merge |
| --- | --- | --- |
| Pinned or forked generator | Changelogs describe current releases, not your frozen fork | Regenerate locally; confirm the null union appears in the emitted types |
| Custom Mustache templates | Branches on the deleted keyword now match nothing | Grep templates for the keyword; rewrite conditions against the type array |
| Dual-dialect publishing | Type arrays are invalid in the prior major dialect | Ship a separately converted artifact; never one document for both |
| Mixed validation gates | A lenient linter passes what a strict validator rejects | Add a Spectral-style rule that fails any document containing the keyword |
| Framework-emitted specs | Hand edits vanish on the next regeneration | Fix the emitter configuration, not the generated file |

![deleted removed archive only, photo 2](https://static.mm-ais.com/article-images-pixabay/openapi-3-one-nullable-keyword-deleted-t-b2725b76.jpg)
deleted removed archive only, photo 2

## What the Release Notes Hide

A green docs portal is a sampling artifact, not a validation result. Lenient parsers read *nullable: true* inside a 3.1 document as an unrecognized annotation and render the page anyway, so the population of "working" 3.1 documents over-represents files that have never met a strict validator or a regenerated SDK. That is survivorship bias with a linter-shaped filter: every compatibility anecdote gathered from a rendering portal systematically understates latent breakage, because the documents that would fail generation were excluded from the sample before anyone counted them. Rendering is not validity — the first real null the API sends finds the non-nullable SDK type and crashes it.

The same bias runs through generator marketing. According to OpenAPI Generator's 7.x release notes, 3.1 support is declared at the framework level, but execution lives in dozens of per-language templates maintained by separate volunteers, and issue trackers for the less common templates carry documented cases of mishandling two-element type arrays — typically collapsing ["string", "null"] into a bare Object or dropping the property outright. Headline-language success does not transfer to the tail. Before any merge, run your project's exact generator invocation, same flag and same template, and require the null union to appear visibly in the SDK diff.

Even a converged toolchain leaves semantic residue the spec never resolves. A property typed ["string", "null"], listed in no required array and given no default, legally occupies three runtime states: absent, null, or a string. Each SDK language maps those states differently — Kotlin emits a nullable String with a null default, Java wraps the field in JsonNullable to keep absent distinct from null, Python collapses both into a None sentinel unless the model layer separates them. Teams consuming byte-identical specifications therefore get divergent client ergonomics and divergent reader-facing reference documentation, where one portal prints "string or null" and another prints nothing at all.

Beneath the convergence record sits a denominator problem. Setting aside the provenance caveat covered previously, nobody publishes a census of live 3.1 documents still containing the removed keyword — the adoption timeline in this guide is assembled from vendor release notes and maintainer announcements at FastAPI, springdoc-openapi, Redocly, and SmartBear, meaning converging projects describing themselves. Every prevalence claim built on that record is extrapolation. The only trustworthy count is local: search your own repositories for the nullable token and treat the hit count as your real migration backlog.

A completed migration can also quietly undo itself. Downgrade converters and translation proxies that emit 3.0 artifacts for legacy consumers re-synthesize nullable: true from type arrays automatically — correct behavior for a downgrader — but when that artifact is committed back as source, the regression lands unnoticed. Without CI enforcement the migration is not sticky: fail any build in which a tracked 3.1 file matches the nullable token, and erosion stops at the proxy boundary where it belongs.

One gap stays honestly open: no controlled study has measured whether developers reading ["string", "null"] infer nullability as reliably as they did with nullable: true. A schema-literal comprehension study is in design at Carnegie Mellon specifically to close it, and until it reports, readability objections to type arrays rest on anecdote — anecdote that the divergent portal prose above makes harder to dismiss than either camp admits.

| Hidden failure | Where it hides | Detection signal | Merge-gate action |
| --- | --- | --- | --- |
| Survivorship bias | Lenient renderers and hosted portals | Spec renders, never met a strict validator | Redocly or Spectral strict lint on every pull request |
| Template tail | Per-language templates behind OpenAPI Generator 7.x | Framework-level release-note claims | Regenerate with your exact settings; require the null union in the diff |
| Three-state residue | Optional properties typed with a base-plus-null array | Absent, null, and value indistinguishable downstream | Set required and default deliberately per property |
| Round-trip erosion | Downgrade converters and translation proxies | Re-synthesized nullable keyword in artifacts | Fail CI on any nullable token in tracked specs |
| Comprehension gap | Reader-facing reference pages | Nullability prose varies by renderer | Standardize one nullability sentence in the doc pipeline |

Ranked by leverage, the CI keyword check wins: one inexpensive gate enforces the merge rule above across three failure modes at once, catching survivorship artifacts, tail-template regressions, and round-trip erosion in a single pass.

![What the Release Notes Hide — OpenAPI 3](https://static.mm-ais.com/article-images-pixabay/openapi-3-one-nullable-keyword-deleted-t-8add4772.jpg)

## Worked Case

Count before you edit. Take a payments-platform description and inventory its component schemas before you touch the file.

```

## Quick answers

| What happened to the nullable keyword in OpenAPI 3.1.0? | It was removed from the specification entirely, and under JSON Schema's core semantics an unrecognized keyword is treated as an ignorable annotation rather than a constraint, so lenient parsers file it away as metadata and quietly discard it. |
| --- | --- |
| What syntax replaced nullable in OpenAPI 3.1? | A grammar extension where type accepts either a single string or an array of strings, so type: ["string", "null"] expresses exactly what 3.0 needed a vendor keyword to express. |
| How long did the most widely deployed API documentation viewer fail to render documents conforming to OpenAPI 3.1.0? | Twenty-seven months after the specification shipped. |
| What happens to generated SDK signatures when a nullable keyword is discarded instead of migrated? | Signatures have no null member at all — TypeScript gets string instead of string \| null, causing a runtime type error on the first null payload, while Kotlin gets String instead of String? and Python gets str instead of Optional[str]. |
| Why does Atlassian run its Jira Cloud pre-migration checklist alongside its Migration Assistant? | Because the tool "won't check for everything," so its numbered table marks every action Mandatory, beginning with creating a migration plan. |

Also worth reading: **OpenAPI 4.0's nullable: A Parse Failure Disguised as Spec Upgrade**: [OpenAPI 4.0's nullable: A Parse](https://specswriter.com/blog/openapi-40s-nullable-a-parse-failure-disguised-as-spec-upgrade.php) · **Double Trouble: Navigating the Pitfalls and Payoffs of Having a Co-Founder**: [Double Trouble: Navigating the Pitfalls](https://specswriter.com/blog/double_trouble_navigating_the_pitfalls_and_payoffs_of_havin.php) · **7 Most Accurate AI Paraphrasing Tools CPU Load and Quality Test Results 2024**: [7 Most Accurate AI Paraphrasing](https://specswriter.com/blog/7_most_accurate_ai_paraphrasing_tools_cpu_load_and_quality_t.php)

### Related reading

- [OpenAPI 4.0's nullable: A Parse Failure Disguised as Spec Upgrade](https://specswriter.com/blog/openapi-40s-nullable-a-parse-failure-disguised-as-spec-upgrade.php)
- [Municipal Spec Rework −27%, Ambiguity −34%: Which Pipeline?](https://specswriter.com/blog/municipal-spec-rework-27-ambiguity-34-which-pipeline.php)
- [Mintlify Agent Markdown: Verified Token Savings and Real Limits](https://specswriter.com/blog/mintlify-agent-markdown-verified-token-savings-and-real-limits.php)
- [ST 2110 vs SDI: Latency, Complexity, and Retrofit Costs](https://specswriter.com/blog/st-2110-vs-sdi-latency-complexity-and-retrofit-costs.php)
- [S-Log3 vs Rec.709: ISO 1600 Fidelity & Workflow Tradeoffs](https://specswriter.com/blog/s-log3-vs-rec709-iso-1600-fidelity-workflow-tradeoffs.php)
- [Copper BOM: Re-verify or Re-price? 30-Day Lag Index Guide](https://specswriter.com/blog/copper-bom-re-verify-or-re-price-30-day-lag-index-guide.php)

### Latest

- [Municipal Spec Rework −27%, Ambiguity −34%: Which Pipeline?](https://specswriter.com/blog/municipal-spec-rework-27-ambiguity-34-which-pipeline.php)
- [Mintlify Agent Markdown: Verified Token Savings and Real Limits](https://specswriter.com/blog/mintlify-agent-markdown-verified-token-savings-and-real-limits.php)
- [ST 2110 vs SDI: Latency, Complexity, and Retrofit Costs](https://specswriter.com/blog/st-2110-vs-sdi-latency-complexity-and-retrofit-costs.php)

Canonical: https://specswriter.com/blog/openapi-3-one-nullable-keyword-deleted-three-encodings-win.php
Markdown: https://specswriter.com/blog/openapi-3-one-nullable-keyword-deleted-three-encodings-win.php/index.md
