JSON Schema Keywords by Draft
JSON Schema keywords changed between draft-07, 2019-09, and 2020-12: definitions became $defs, tuples moved to prefixItems, and keywords beside $ref started to apply. This sheet lists those differences, shows how to write optional and nullable fields, and explains when format is checked. The optional, nullable, and format tables match what the JSON Schema generator emits.
What changed between drafts
| Feature | draft-07 | 2019-09 | 2020-12 |
|---|---|---|---|
| $schema URI | http://json-schema.org/draft-07/schema# | https://json-schema.org/draft/2019-09/schema | https://json-schema.org/draft/2020-12/schema |
| Reusable definitions | definitions | $defs | $defs |
| Tuples | items as an array, plus additionalItems | items as an array, plus additionalItems | prefixItems, plus items for the rest |
| Keywords beside $ref | Ignored | Applied | Applied |
| Dependencies | dependencies | dependentRequired and dependentSchemas | dependentRequired and dependentSchemas |
| Recursive references | None (plain $ref to #) | $recursiveRef and $recursiveAnchor | $dynamicRef and $dynamicAnchor |
| unevaluatedProperties, unevaluatedItems | No | Yes | Yes |
| minContains, maxContains | No | Yes | Yes |
| Plain-name anchors | $id: "#name" | $anchor | $anchor |
| format | Left to the implementation | Annotation by default. A validator may offer an option to assert it | Annotation by default. The format-assertion vocabulary or a validator option turns checking on |
| OpenAPI | Not used by OpenAPI 3.x (3.0 has its own dialect based on draft 5, with nullable) | Not used by any OpenAPI version | OpenAPI 3.1 uses 2020-12 |
Optional and nullable
| What the samples showed | JSON Schema |
|---|---|
| Key in every sample, never null | "type": "string", and the key is listed in required |
| Key missing from some samples | "type": "string", and the key is left out of required |
| Key always present, sometimes null (simple type) | "type": ["string", "null"], and the key is listed in required |
| Key always present, sometimes null (reused shape) | "anyOf": [{ "$ref": "#/$defs/Address" }, { "type": "null" }], and the key is listed in required |
| Key missing somewhere and null somewhere | "type": ["string", "null"], and the key is left out of required |
| OpenAPI 3.0 nullable: true | Not part of JSON Schema. Many validators ignore it, while Ajv (which the generator uses) accepts it as an extension when type is present. Portable JSON Schema, including OpenAPI 3.1, uses "type": ["string", "null"] or anyOf with null |
required means the key must be present. It says nothing about the value, so a required key can hold null when its type allows it. The anyOf form is used around $ref because draft-07 ignores keywords beside $ref, and 2019-09 and later apply them as extra constraints, so neither can widen a $ref to also allow null. anyOf with a null branch does.
format values the generator detects
| format | Example | Detected when | Checked when |
|---|---|---|---|
| date-time | 2024-03-01T09:30:00+05:30 | Every value is an RFC 3339 date and time with an offset (Z, +hh:mm, or -hh:mm, at most 23:59) | Only when format assertion is on |
| date | 2024-02-29 | Every value is yyyy-mm-dd and a real calendar day, leap years included | Only when format assertion is on |
| [email protected] | Every value is a plain address with a dotted domain of two or more labels | Only when format assertion is on | |
| uri | https://example.com/a?b=1#c | Every value is an http or https URL with a plain host name | Only when format assertion is on |
| uuid | 7c9e6679-7425-40de-944b-e07fc1f90ae7 | Every value is 8-4-4-4-12 hexadecimal, in either case | Only when format assertion is on |
| ipv4 | 192.168.0.1 | Every value is four dotted octets from 0 to 255 with no leading zeros | Only when format assertion is on |
A format is added only when every string seen in that position matched it. One value that does not match leaves the field as a plain string. In 2019-09 and 2020-12 a validator treats format as an annotation unless it is configured to assert it, which is what the Assert formats switch in the generator does.
Frequently asked questions
What does $schema do?
It names the draft (the dialect) the schema is written in, so a validator knows which keywords exist and what they mean. The same schema can validate differently under draft-07 and 2020-12, for example when keywords sit beside $ref. Leave it out and the validator falls back to its own default, which may not be the draft you wrote for.
Can I use a 2020-12 schema in OpenAPI 3.0?
Not as is. OpenAPI 3.0 uses its own subset of an older draft: type must be a single string, null is expressed with nullable: true, and keywords such as $defs, prefixItems, and const are not part of it. OpenAPI 3.1 aligned with 2020-12, so a 2020-12 schema fits a 3.1 document. One catch: a reference such as "#/$defs/Address" in a schema with no $id of its own resolves against the OpenAPI document root, where there is no $defs. Turn off Reuse repeated shapes in the generator before pasting into components/schemas, or rewrite "#/$defs/X" to "#/components/schemas/X".
What is the difference between $defs and definitions?
Both hold reusable subschemas that $ref points at, and neither affects validation on its own. definitions is the draft-07 name, from the validation specification. 2019-09 renamed it to $defs and moved it into the core vocabulary. A validator for the newer drafts still resolves a reference into definitions, since $ref works on any JSON Pointer, but new schemas should use $defs.