OpenAPI 3.1's JSON Schema 2020-12 Alignment: What Actually Changes for Validation
OpenAPI 3.1 uses JSON Schema 2020-12, letting teams validate docs and runtime requests with one schema. nullable becomes type unions, exclusive bounds become numeric, and examples move to media type. Toolchain upgrades are required.
12 May 2026, 10:33 UTC

The problem: validation drift between docs and runtime
Teams using OpenAPI 3.0.x know the frustration: the schema in your spec validates in Swagger UI, but your Express middleware needs a slightly different shape because nullable: true isn't standard JSON Schema. You maintain two copies — one for documentation, one for request validation — and they inevitably drift.
OpenAPI 3.1 (February 2021) adopts JSON Schema 2020-12 as its schema dialect. That means the exact same schema object can be fed to a standard JSON Schema validator (AJV, Hyperjump, java-json-schema-validator) and to OpenAPI tooling. No more translation layer.
What changed in the schema dialect
Nullable becomes a union type
In 3.0 you wrote:
components:
schemas:
User:
type: object
properties:
middleName:
type: string
nullable: true
In 3.1, nullable is invalid. You use a type array:
components:
schemas:
User:
type: object
properties:
middleName:
type: ["string", "null"]
Standard JSON Schema 2020-12 validators understand this natively. If you're migrating with a script, every nullable: true must become type: ["originalType", "null"] — including inside allOf/anyOf.
Exclusive bounds are now numeric
3.0 used booleans:
minimum: 0
exclusiveMinimum: true
3.1 follows JSON Schema 2020-12:
minimum: 0
exclusiveMinimum: 0
The value of exclusiveMinimum is the exclusive bound itself, not a flag. Same for exclusiveMaximum.
Example moves to media-type level
Schema-level example is gone. You now put examples on the media type:
paths:
/users:
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
example:
firstName: "Ada"
lastName: "Lovelace"
middleName: null
This aligns with the examples object (plural) that supports multiple named examples with summaries.
Worked example: polymorphic payload with richer constraints
Suppose you accept a payment object that varies by method. In 3.0 you'd use discriminator with inline schemas. In 3.1 you can keep the discriminator mapping but add if/then/else inside each mapped schema for cross-field validation.
components:
schemas:
PaymentRequest:
oneOf:
- $ref: '#/components/schemas/CardPayment'
- $ref: '#/components/schemas/BankTransfer'
discriminator:
propertyName: method
mapping:
card: '#/components/schemas/CardPayment'
bank: '#/components/schemas/BankTransfer'
CardPayment:
type: object
required: [method, cardNumber, expiryMonth, expiryYear]
properties:
method:
type: string
enum: [card]
cardNumber:
type: string
pattern: '^\d{13,19}$'
expiryMonth:
type: integer
minimum: 1
maximum: 12
expiryYear:
type: integer
minimum: 2024
if:
properties:
cardNumber:
pattern: '^4'
then:
properties:
cardType:
const: "visa"
else:
properties:
cardType:
const: "other"
BankTransfer:
type: object
required: [method, accountNumber, routingNumber]
properties:
method:
type: string
enum: [bank]
accountNumber:
type: string
routingNumber:
type: string
pattern: '^\d{9}$'
The if/then/else block is pure JSON Schema 2020-12. A standard validator will enforce that Visa cards (starting with 4) must have cardType: "visa", while other cards get "other". Your Fastify or Spring middleware can load this exact schema — no translation.
Trade-off: toolchain coordination required
You cannot drop a 3.1 document into Swagger UI 4.x or OpenAPI Generator 5.x. The ecosystem moved in steps:
- Swagger UI 5.0+ (mid-2022) reads 3.1
- OpenAPI Generator 6.0+ (late 2021) emits 3.1 client code
- Redocly CLI 1.0+ lint and bundle 3.1
If your CI pipeline runs swagger-codegen 3.x, it will fail on a 3.1 spec. Migration is a coordinated upgrade across documentation, client generation, and server validation middleware. Plan a flag day or run dual specs during transition.
Practical verification steps
- Validate dialect compliance: Run your 3.1 schema through AJV with the 2020-12 draft:
Run in your project root with Node 18+. No elevated permissions. Expectnpm i ajv@8 ajv-formats node -e " const Ajv = require('ajv'); const ajv = new Ajv({ strict: false, draft: '2020-12' }); const schema = require('./openapi.json'); const userSchema = schema.components.schemas.User; const validate = ajv.compile(userSchema); console.log('Valid schema:', validate({ firstName: 'Test', middleName: null })); "trueif the union type works. Risk: false confidence if you only test happy paths — add negative cases. - Check for stray
nullable: Grep your spec:
Should return nothing after migration.grep -r "nullable: true" openapi.yaml - Test discriminator + if/then/else: Feed the
PaymentRequestschema above to your validator with a Visa card missingcardType. Expect validation error. This confirms the conditional logic runs in standard validators.
Closing: adopt when your toolchain is ready
The engineering win is real: one schema artifact for docs, client generation, and runtime validation. But the migration tax is paid in tool upgrades, not syntax tweaks. If your stack already runs Swagger UI 5+, OpenAPI Generator 6+, and a 2020-12-compatible validator, the switch eliminates a whole class of drift bugs. If not, stage the upgrade — start by converting nullable to unions in a branch and validating with AJV 2020-12. That alone catches the most common breaking change before you touch CI.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.