# JSON Schema 2020-12 in OpenAPI 3.1: what changed from 3.0

> A schema should accept the real examples and refuse what the rules forbid. What 2020-12 changes, what OpenAPI 3.1 inherits, and a test built to fail.

Published 2026-10-30 · https://aiskills402.com/blog/json-schema-2020-12-openapi-3-1

A schema is only useful if it decides correctly in both directions: it has to accept every legitimate document, including ones nobody has shown you yet, and refuse the ones the rules forbid. Copying the shape of one example gets the first half wrong, because an example proves what exists and not what is allowed. This guide goes through the keywords that decide those two outcomes in JSON Schema 2020-12, then through what changes when the schema is placed inside an OpenAPI 3.1 document ([the migration notes](https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0)), and finishes with a test built to fail.

## Start with the rules, then the examples

Before writing a keyword, point to the sentence in the requirements that calls for it. A key present in every sample is probably required, while a key absent from even one sample is optional, and a lone sample proves nothing about requirement. Two observed values of a field do not make a closed set, and numbers that happen to be whole do not make an integer, since a price that shows `10` today may show `10.50` tomorrow. The specification is plain about what `required` means: an object is valid when every listed name is a property of the instance, and omitting the keyword is the same as an empty list ([JSON Schema validation](https://json-schema.org/draft/2020-12/json-schema-validation)). Nothing in it infers a requirement from what was seen.

## The keywords that decide acceptance

**`type`** names the kind of value, and an array of names allows several. A field that may hold null is `["string", "null"]`. The core document notes that the data model makes no distinction between integers and other numbers, so `integer` is a constraint a validator applies to the value, not a separate JSON type ([JSON Schema core](https://json-schema.org/draft/2020-12/json-schema-core)).

**`enum` and `const`** close a set. `enum` accepts an instance that equals one of its elements, and elements can be of any type including null; `const` is the same with a single value ([JSON Schema validation](https://json-schema.org/draft/2020-12/json-schema-validation)). Reach for them when the requirements close the set, never because a few values turned up in samples.

**`additionalProperties`** applies only to the names that `properties` and `patternProperties` do not cover in the same schema object, and omitting it behaves like an empty schema, meaning anything extra passes ([JSON Schema validation](https://json-schema.org/draft/2020-12/json-schema-validation)). Switch it to `false` only if unknown fields are explicitly forbidden. Real APIs add fields over time, and a closed schema turns that into a rejection.

**`unevaluatedProperties`** exists for the case where `additionalProperties` cannot see far enough. The first one looks only at its siblings; the second also uses the annotations produced by adjacent in-place applicators such as `allOf`, so a closed object assembled from several parts can be closed in one place ([JSON Schema core](https://json-schema.org/draft/2020-12/json-schema-core)). If an `allOf` of two shapes with `additionalProperties: false` in each rejects everything, that is the reason.

**`format`** is where schemas most often promise more than they deliver. In 2020-12 the keyword is collected as an annotation, and implementations may also treat it as an assertion, but they must provide switches for that and it must be off by default ([JSON Schema validation](https://json-schema.org/draft/2020-12/json-schema-validation)). So when a malformed date must be refused, add a `pattern` too, or configure the validator to assert formats.

**Tuples** changed. A fixed-position array is now written with `prefixItems`, which takes a list holding one schema for each slot, and `items` applies to whatever comes after that prefix. The keyword does not limit the length by itself, so `items: false` plus `minItems` makes a pair mean exactly two ([JSON Schema core](https://json-schema.org/draft/2020-12/json-schema-core)). The release notes explain the reason: the old `items` was used both for arrays and for tuples, and people defined a tuple when they meant an array and saw only the first element checked ([release notes](https://json-schema.org/draft/2020-12/release-notes)). In 2019-09 and earlier, an `items` array did the work that `prefixItems` does now.

```json
{
  "type": "object",
  "required": ["id", "status"],
  "properties": {
    "id": { "type": "integer" },
    "status": { "enum": ["open", "closed"] },
    "note": { "type": ["string", "null"] },
    "point": {
      "type": "array",
      "prefixItems": [{ "type": "number" }, { "type": "number" }],
      "items": false,
      "minItems": 2
    }
  },
  "additionalProperties": false
}
```

## What OpenAPI 3.1 changes

OpenAPI 3.0 used its own cut of JSON Schema, a subset with additions. In 3.1 the specification calls the Schema Object a superset of JSON Schema draft 2020-12 and says unspecified behaviour follows JSON Schema ([OpenAPI 3.1.0](https://spec.openapis.org/oas/v3.1.0.html)). The migration notes from the OpenAPI Initiative list the practical effects ([migration guide](https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0)):

- `nullable` was removed instead of deprecated. A 3.0 file said `type: string` plus `nullable: true`; the 3.1 form is a type array holding `string` and `null`, and a branch of `anyOf` or `oneOf` whose type is null also works ([migration notes](https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0)). A tool that follows 3.1 skips a leftover `nullable`, so null values get refused.
- `exclusiveMinimum` and `exclusiveMaximum` are numbers now, not booleans that modify `minimum`.
- A single `example` inside a schema gives way to the JSON Schema keyword `examples`, a list. Media types and parameters keep their own `example` and `examples` fields.
- File uploads are described with `contentEncoding` and `contentMediaType`, not with `format: binary`.
- `$schema` is allowed, so a schema file can declare another dialect.

Changing only the version number of a 3.0 file does not give a faithful 3.1 file: it still loads, yet null handling, exclusive limits and examples now mean something else ([migration notes](https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0)).

## Parameters, request bodies and responses

The operation around the schema has rules of its own. Each parameter has a `name` and a location called `in`, with four possible values (query, header, path, cookie). For a path parameter the `required` flag must be present and true; for the others it defaults to false ([OpenAPI 3.1.0](https://spec.openapis.org/oas/v3.1.0.html)). The reason is that a missing segment would change the URL. The `example` field of a parameter is mutually exclusive with `examples`, and the specification says a parameter definition named `Accept`, `Content-Type` or `Authorization` in a header is ignored, since security schemes and media types cover those.

A request body has a required `content` map keyed by media type, with `required` defaulting to false, so a body that must be sent needs the flag. Responses are keyed by status code, and the key must be a quoted string for compatibility between JSON and YAML; ranges such as `2XX` are allowed, and an explicit code beats a range ([OpenAPI 3.1.0](https://spec.openapis.org/oas/v3.1.0.html)). The specification expects the documentation to cover a successful response and known errors, not every possible code. Adding a `404` that nobody showed would assert something about the server that nobody observed.

A type also follows the rules and not the transport: a path value travels as text in a URL, but an identifier the rules call a whole number is an `integer` in the schema. For a query list, repeated names (`ids=1&ids=2`) are the default `form` style with `explode` true, while one comma-joined value needs `explode: false`. Both follow from reading the example literally rather than assuming another style.

## A test built to fail

Validate a set of documents against the schema and make the check able to fail. Collect the real examples, which must pass, and add documents that each violate exactly one rule: an enum value outside the set, an extra field, a wrong tuple length, a missing required key, a malformed date. With a JavaScript validator (Ajv, using its 2020-12 build) a small run on the schema above accepted both good documents and rejected the unknown status, the extra field, the three-element point and the missing `id`. A `created` field with `"not a date"` and `format: "date-time"` also passed, because the validator ignored the format without its formats add-on. That last result is the useful one: the check could fail, and it showed that the schema did not enforce what it appeared to.

Repeat the idea for an OpenAPI document: use a validator that understands the 3.1 format, then check the examples embedded in it, because nothing validates those on its own ([OpenAPI 3.1.0](https://spec.openapis.org/oas/v3.1.0.html)). For the schema, keep the deliberately bad file in the test suite; a suite that only holds good documents passes for any schema, including `{}`.

Before tightening a schema that clients already use, read [which edits count as breaking in an OpenAPI spec](/blog/openapi-breaking-changes), since a stricter rule on a response is not the same as a stricter rule on a request.

## When this does not apply

The statements here come from the 2020-12 specification and the OpenAPI 3.1.0 text as published ([OpenAPI 3.1.0](https://spec.openapis.org/oas/v3.1.0.html)). Older drafts behave differently in the places named, and OpenAPI 3.0 files, later 3.1 patch releases and validators with their own options were not compared. The validator run was a single small test, not a survey of tools, and different validators or versions may report differently, especially for formats. Schemas generated from examples can be made stricter than the rules ask for, and nothing here replaces a conversation about which fields are really mandatory.

## Two tools for the schema and the operation

Our [json-schema-from-example](https://aiskills402.com/skills/json-schema-from-example) skill reads sample documents plus requirements written in words and answers with a bare schema in the newest dialect covered above. It marks a key mandatory only when all samples carry it or a requirement says so, closes an object or a value set only on request, writes arrays with fixed positions through `prefixItems`, and expresses a nullable field as a list of types. If a requirement contradicts a sample, the requirement wins.

For the operation, our [openapi-operation-from-example](https://aiskills402.com/skills/openapi-operation-from-example) skill takes a request, a response and the rules and returns a single operation object in the 3.1 format ([OpenAPI 3.1.0](https://spec.openapis.org/oas/v3.1.0.html)). It marks path parameters as mandatory, writes nullable values as type lists, quotes the status keys, includes only the responses that were actually shown, and keeps an authorization header out of the parameter list. Declared as a parameter, that header would be ignored anyway.
