TS-29: JSON Schema
This technical standard sets out some guidelines for JSON schema. The emphasis on this technical standard is on designing JSON schema, though there is also some information on using published JSON schemas such as JSON-LD.
JSON Schema provides a vocabulary for describing the structure of JSON data. Documentation, code, and other artifacts can be generated from JSON Schema definitions, but the primary use case if to validate JSON documents.
JSON schema is commonly used to define the structure of data transferred between systems via network APIs. It may also be used for input validation, to define interfaces for data structures constructed at runtime, and to define data persistence schema (eg. document stores).
- Content type
- Versions
- Validation keywords
- Applicator keywords
- Schema identification
- Unevaluated keywords
- Conditional validation
- Annotations
- Format
- Content keywords
- Cross-references
- JSON Hyper-Schema
- OpenAPI
- JSON-LD (JSON for Linked Data)
- JSON Pointer
- JSON Type Definition (JTD)
- Schema versioning
- Best practices
- Modeling patterns
- References
- Useful links
Content type
The official content type for JSON Schema is application/schema+json. It is
RECOMMENDED to use this content type when serving JSON Schema via HTTP.
Versions
JSON Schema was originally defined as an IETF standard, but is now maintained as a community project. Versions 00 through 04 were published as IETF drafts, while draft-05 moved to the community project.
The current version of JSON Schema is Draft 2020-12. It is RECOMMENDED to use
this version for new projects. The metaschema for this version is
https://json-schema.org/draft/2020-12/schema.
Validation keywords
The Validation vocabulary is the core of JSON Schema: the keywords that make
an assertion about an instance and either pass or fail it. It is RECOMMENDED
to prefer these keywords over ad-hoc pattern regular expressions wherever a
dedicated keyword exists, since a dedicated keyword is easier for both humans
and tooling to interpret correctly.
Type and value keywords
type restricts an instance to one JSON type, or an array of types:
{ "type": "string" }
{ "type": ["string", "null"] }enum restricts an instance to one of a fixed set of values, of any type.
const restricts an instance to exactly one value, and is equivalent to an
enum with a single member. It is RECOMMENDED to use const rather than a
single-value enum, since it states the intent — a fixed, non-negotiable
value — more directly:
{ "enum": ["draft", "published", "archived"] }
{ "const": "2020-12" }String keywords
maxLength and minLength bound the length of a string instance, counted in
Unicode code points, not bytes. pattern requires the string to match an
ECMA-262 regular expression
(unanchored — wrap in ^ and $ to match the whole string).
Numeric keywords
maximum and minimum bound a numeric instance inclusively;
exclusiveMaximum and exclusiveMinimum bound it exclusively.
multipleOf requires the instance to be an integer multiple of the given
value.
Object keywords
required lists property names that MUST be present. dependentRequired
extends this conditionally — see Conditional validation.
maxProperties and minProperties bound the number of properties on an
object instance.
Array keywords
maxItems and minItems bound the length of an array instance. uniqueItems
requires every element to be distinct when set true. maxContains and
minContains bound how many elements the contains applicator (see
Applicator keywords) is permitted to match; used without contains, both
are ignored.
{
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"maxItems": 10,
"uniqueItems": true
}Applicator keywords
The Applicator vocabulary applies subschemas to all or part of an instance. These are the keywords that give JSON Schema its compositional structure — combining, narrowing, or branching between subschemas rather than asserting directly on a value.
Boolean composition
allOf, anyOf, oneOf, and not combine subschemas using boolean logic:
an instance must validate against all of `allOf’s subschemas (AND), at least
one of `anyOf’s (OR), exactly one of `oneOf’s (XOR), or must fail `not’s
subschema.
It is RECOMMENDED to prefer schema composition over inheritance — see
Cross-references for composing schemas across files with $ref, and
Schema identification for $defs, inlining reusable subschemas within
one file. allOf is the most common of the four, used to merge a base
schema with extensions:
{
"allOf": [
{ "$ref": "base-entity.schema.json" },
{
"properties": {
"email": { "type": "string", "format": "email" }
}
}
]
}oneOf is appropriate where an instance must match exactly one of several
alternative shapes — for example, a discriminated union. anyOf is looser,
and it is RECOMMENDED to prefer oneOf wherever the alternatives are meant
to be mutually exclusive, since anyOf will silently accept an instance that
happens to match more than one branch.
Object and array applicators
properties validates named object properties against per-property
subschemas. patternProperties does the same for properties whose name
matches a regular expression, and additionalProperties constrains any
property not matched by either — commonly set to false to reject unknown
properties, or to a schema to constrain them. propertyNames validates every
property name itself against a subschema, independent of its value.
items validates array elements against a subschema. prefixItems validates
a fixed-length prefix of an array positionally, one subschema per position,
with items then constraining any remaining elements. contains requires at
least one array element to validate against its subschema; combine with
minContains and maxContains (see Validation keywords) to bound how
many must match.
dependentSchemas applies a subschema to the whole instance conditionally,
based on the presence of a property — see Conditional validation.
Schema identification
The Core vocabulary is the set of keywords the JSON Schema specification
itself depends on, independent of any other vocabulary — schema identity,
inline reuse, and dynamic referencing. $ref (see Cross-references) is
also part of this vocabulary; the keywords below are its companions.
Document skeleton
A schema document SHOULD begin with $schema and $id:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/product.schema.json",
"title": "Product",
"description": "A single catalogue item.",
"type": "object"
}$schema declares which draft the document is written against (see
Versions) and is REQUIRED at the root of every schema document, so that
validators and tooling know which keyword set to apply. $id declares the
schema’s own canonical URI. It is REQUIRED at the root of every schema
document that is referenced from elsewhere via $ref, since $ref
resolution is relative to the nearest enclosing $id; it is OPTIONAL, and
NOT RECOMMENDED, on a schema that only ever appears inline. title and
description (see Annotations) commonly follow $id, then type and
the rest of the schema’s constraints.
$comment
$comment attaches a note to a schema for maintainers — for example, why a
constraint exists, or a link to the ticket that introduced it. Unlike
description, which is annotation intended for schema consumers, $comment
is not surfaced to consumers and tooling MAY strip it. It is RECOMMENDED
over an ad-hoc convention such as a //-prefixed property, since a
non-standard key risks being interpreted as data by a validator that does
not know to ignore it.
$defs
$defs is a reserved location for subschemas meant to be referenced by
$ref, rather than validated directly. It is RECOMMENDED for reusable
subschemas that are only ever consumed from within the same document —
where Cross-references' file-per-schema, $ref-to-sibling-file pattern
would be excessive. Combine a $defs reference with allOf (see
Boolean composition) to merge a shared subschema into several
properties:
{
"$id": "https://example.com/schemas/order.schema.json",
"type": "object",
"properties": {
"billingAddress": { "$ref": "#/$defs/address" },
"shippingAddress": { "$ref": "#/$defs/address" }
},
"$defs": {
"address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"postalCode": { "type": "string" }
},
"required": ["street", "city", "postalCode"]
}
}
}A subschema under $defs is not itself applied to the instance being
validated — only a $ref to it is. Prefer $defs over duplicating a
subschema at each point of use, for the same reason duplicated code is
avoided elsewhere: one definition, updated once, without the risk of a
partial edit leaving the copies out of sync.
$anchor, $dynamicAnchor, and $dynamicRef
$anchor names a location within a schema document so it can be targeted by
a fragment $ref ("$ref": "anchor-name") without depending on the
subschema’s structural path — useful because a structural path
(/$defs/address) breaks if the subschema is moved, while a named anchor
does not.
$dynamicAnchor and $dynamicRef are a specialized pair for building
extensible, recursive schemas — for example, a base "node" schema that a
downstream schema extends with additional properties, where the recursive
reference must resolve against the extending schema, not the schema that
declared it. This is NOT RECOMMENDED for general-purpose schema reuse:
$dynamicRef resolution depends on the full chain of schemas in scope at
validation time, which is harder to reason about than a plain $ref, and
most validators implement it more strictly, and with more edge-case bugs,
than plain referencing. Reach for it only when building a schema meant to be
extended by others, such as a shared base vocabulary; prefer plain $ref
and $anchor otherwise.
A plain, non-recursive self-reference — a schema that refers to itself via
$ref to validate a recursive data structure such as a tree — needs none of
this: $ref to the document’s own root ("$ref": "#") or to a $defs
entry via $anchor is sufficient, and is RECOMMENDED over $dynamicRef for
that case.
$vocabulary
$vocabulary declares which sets of keywords a meta-schema requires or
allows, and is how the Format Assertion vocabulary (see Format) is
opted into. It is used when authoring a meta-schema — a schema that
constrains other schemas — which is NOT RECOMMENDED for most projects. Most
schema authors never need to write one, and existing meta-schemas such as
Draft 2020-12’s default cover ordinary use. Where a meta-schema is genuinely
needed, $vocabulary MUST be declared at its root, listing each vocabulary
URI and whether it is required (true) or optional (false).
Unevaluated keywords
unevaluatedProperties and unevaluatedItems constrain whatever an
instance’s properties or array elements were not already matched by any
other applicator in the schema — including subschemas reached via allOf,
$ref, or a conditional if/then branch (see Conditional validation).
This is a broader net than additionalProperties and items (see
Applicator keywords), which only see the sibling keywords in their own
schema object and are blind to properties matched by a composed subschema.
The difference matters wherever allOf composes a base schema with
extensions. additionalProperties: false on the extension alone rejects the
base schema’s own properties, because from the extension’s perspective they
are unrecognized:
{
"allOf": [
{ "$ref": "base-entity.schema.json" }
],
"properties": {
"email": { "type": "string", "format": "email" }
},
"unevaluatedProperties": false
}unevaluatedProperties: false here correctly rejects any property not
defined by either the base schema or this extension, once both have been
evaluated — which additionalProperties: false cannot do, since it would
have to be duplicated onto the base schema and would then reject the very
properties the extension adds.
It is RECOMMENDED to use unevaluatedProperties: false in place of
additionalProperties: false wherever a schema is composed via allOf,
$ref, or a conditional, and to keep additionalProperties: false only on
schemas with no such composition, where the two behave identically but
additionalProperties is the simpler, more widely-supported keyword.
unevaluatedItems follows the same logic for arrays composed via
prefixItems and contains across multiple subschemas.
Conditional validation
JSON Schema supports two distinct mechanisms for applying validation conditionally, based on the value or presence of another part of the instance.
if/then/else
if, then, and else behave like a programming conditional: if the
instance validates against the if subschema, it MUST also validate against
then (and else is ignored); otherwise, it MUST validate against else
(and then is ignored). Either then or else MAY be omitted.
{
"if": {
"properties": { "country": { "const": "US" } }
},
"then": {
"properties": { "postalCode": { "pattern": "^[0-9]{5}$" } }
},
"else": {
"properties": {
"postalCode": { "pattern": "^[A-Z][0-9][A-Z] [0-9][A-Z][0-9]$" }
}
}
}Where more than two branches are needed, wrap multiple if/then pairs in
an allOf (see Applicator keywords), one pair per case.
dependentRequired and dependentSchemas
dependentRequired and dependentSchemas both trigger on the presence of
a property, rather than on a value comparison, and are RECOMMENDED over
if/then for that narrower case, since they express the intent more
directly.
dependentRequired requires additional properties to be present when a given
property is present. It is one-way: requiring billing_address when
credit_card is present does not also require credit_card when
billing_address is present.
{
"dependentRequired": {
"credit_card": ["billing_address"]
}
}dependentSchemas applies a whole subschema, not just a list of required
properties, when a given property is present — useful where the dependent
constraint is more than "these properties must exist":
{
"dependentSchemas": {
"credit_card": {
"properties": {
"billing_address": { "$ref": "address.schema.json" }
},
"required": ["billing_address"]
}
}
}Annotations
The Meta-Data vocabulary annotates a schema without constraining validation: an instance that fails to conform to an annotation still passes, since these keywords describe rather than assert. It is RECOMMENDED to use them consistently, since documentation, code generators, and UI tooling commonly read them.
title gives a short, human-readable label for the schema. description
gives a longer explanation of the instance’s purpose. Both are RECOMMENDED on
every schema intended for reuse or publication.
default supplies a default value to use where the instance omits the
property. It is an annotation, not a substitution — most validators do not
inject the default into the instance being validated.
examples provides one or more sample values illustrating valid instances,
useful in generated documentation.
deprecated, set true, signals that applications SHOULD avoid using the
annotated property, without yet removing it — the schema equivalent of a
deprecation notice. It is RECOMMENDED over deleting a property outright when
retiring part of a schema that existing clients may still send or expect.
readOnly and writeOnly describe the direction data flows: readOnly
marks a property that is managed by the server and MUST NOT be modified by a
client, and writeOnly marks one that is accepted from a client but never
returned — a password field, for example. Both are annotations only; a
schema author who wants to actually reject a client-supplied value for a
read-only property must enforce that outside JSON Schema, for example at the
API layer.
Format
The format keyword names a semantic constraint on a string (or, for a few
values, a number) that goes beyond what pattern and the other Validation
keywords can express concisely — email, date-time, uuid, ipv4, and
similar.
{
"type": "string",
"format": "date-time"
}By default, under Draft 2020-12, format is annotation-only: a validator
is not required to reject an instance that fails a format constraint, only
to report it. This is a common source of surprise, since most schema authors
expect format to behave like the other Validation keywords. It is
RECOMMENDED to explicitly state, in the schema’s description or in
accompanying documentation, whether format is expected to be enforced by
the validator in use, since the specification itself leaves this to
implementation configuration.
Format Assertion is a separate, official vocabulary
(https://json-schema.org/draft/2020-12/vocab/format-assertion) that a
metaschema can opt into to make format failures a validation error rather
than an annotation. It is not included by default. Where consistent
enforcement across implementations matters, it is RECOMMENDED to either
declare the Format Assertion vocabulary explicitly in a custom metaschema, or
to not rely on format for enforcement at all and instead express the
constraint as a pattern or a custom keyword the validator does enforce.
Content keywords
The Content vocabulary annotates a string instance that itself carries non-JSON data — a base64-encoded file, or an embedded JSON document — without JSON Schema attempting to parse or validate that embedded content directly.
contentEncoding names the encoding used to represent binary data as a
string, eg. base64. contentMediaType names the
IANA media
type of the decoded content, eg. image/png or application/json.
contentSchema supplies a JSON Schema that the decoded content must conform
to, applicable only where contentMediaType is itself application/json or
a JSON-based media type.
{
"type": "string",
"contentEncoding": "base64",
"contentMediaType": "application/json",
"contentSchema": {
"$ref": "payload.schema.json"
}
}Like format, these keywords are annotations, not assertions: a validator is
not required to decode and check the embedded content, only to report the
declared encoding, media type, and schema. It is RECOMMENDED to use them
wherever a schema embeds non-JSON data in a string, since they give tooling
and other readers of the schema a documented way to decode and validate that
payload, even where the validator in use does not enforce them automatically.
Cross-references
JSON Schema defines a $ref
keyword to link schema from other schema.
It is RECOMMENDED to use $ref to share common structures, keeping schemas
modular and reusable. Prefer schema composition over inheritance — see
Applicator keywords for the allOf/anyOf/oneOf/not keywords used to
combine referenced subschemas. Designing your schema for modularity is how,
like code, you can best manage growing complexity at scale.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Locale",
"description": "Locale, composed from a BCP-47 language tag, ISO-3166 alpha-2 country code, and Olsen IANA timezone ID.",
"type": "object",
"required": [
"country_code",
"language"
],
"properties": {
"country_code": {
"$ref": "country-code.schema.json"
},
"language": {
"$ref": "language-code.schema.json"
},
"timezone": {
"$ref": "timezone.schema.json"
}
}
}However, as per the JSON Schema documentation, it is RECOMMENDED to not require
client applications to automatically resolve schema that are referenced from
other schema via the $ref attribute. Instead, it is RECOMMENDED that JSON
Schema publishers automatically swap $ref properties for inline schema, using
a build step.
A command-line tool is available to do this.
File organization
It is RECOMMENDED to define one schema per file, named for the entity it
describes in snake_case with a .schema.json suffix (for example,
country_code.schema.json), and to compose schemas across files with a
relative $ref to the sibling file rather than inlining or duplicating
the referenced schema. This mirrors the one-module-per-concept convention
used elsewhere in software, for the same reason: a schema’s file is then
addressable independently, reviewable in isolation, and reusable by $ref
from more than one parent without copying it.
JSON Hyper-Schema
JSON Hyper-Schema is an extension to JSON Schema that adds properties to allow the embedding of hypermedia links in JSON documents. It builds on URI Templates (RFC 6570) and other standards.
The main use case for JSON Hyper-Schema is to embed hypermedia controls in JSON documents that enable APIs to be navigated, and explored in a dynamic way (ie. without clients necessarily having initial knowledge of all available resources and endpoints).
The following example, taken from the specification, adds a single link:
{
"type": "object",
"properties": {
"id": {
"type": "number",
"readOnly": true
}
},
"links": [
{
"rel": "self",
"href": "thing/{id}"
}
]
}JSON Hyper-Schema adds the links property, which must be an array of "link
description objects" (LDO).
Any IANA-registered link relation type can be used to define the link relation
type (rel). This defines the semantics of the link in terms of its
relationship to the resource. The table below describes some of the most
commonly used link relation types.
Type | Description |
|---|---|
| Identifies the resource itself. |
| Indicates the next or previous resource in a sequence, eg. in a paginated list. |
| Refers to the first or last resource in a sequence. |
| Refers to a parent resource in a hierarchy of resources. |
It is RECOMMENDED to use a small subset of link relation types, and to use them consistently across all endpoints in a hypermedia API. You SHOULD NOT define custom relations, unless absolutely necessary when no standard ones fit the use case.
The following best practices also apply for JSON Hyper-Schema:
- Each response should contain minimal links, just enough for clients to navigate deeper.
- Do not expose an entire API graph in any single response, except for simple APIs composed of just a few endpoints.
- It is RECOMMENDED to always include "self" links to the current resource, wherever relevant.
- Use simple, minimalist URI templates, with only necessary path parameters (variables).
- Provide all required template variables in the response object.
- Use
templatePointersto clearly map data to template variables. - Use
targetSchemato describe expected response formats. - Use
targetHintsfor media types and methods. - Add
titleproperties for human-readable link descriptions.
OpenAPI
OpenAPI is another dialect of JSON Schema, used to describe HTTP "RESTful" APIs. It is a superset of JSON Schema, adding additional properties to describe HTTP-specific concepts.
for all JSON Schema, but there are a few OpenAPI-specific things to keep in mind.
JSON-LD (JSON for Linked Data)
JSON-LD is a schema used to imbue JSON documents with semantic meaning and to embed linked data.
JSON-LD bridges the gap between concepts from the semantic web and modern web service APIs. The idea is that machines can understand and explore data in JSON documents, similarly to how semantic web technologies like RDF and OWL work.
JSON-LD defines two main properties:
@contextreferences a vocabulary that describes the concepts in the document.@typeindicates the type of the entity represented by an object.
Schema.org is perhaps the most popular vocabulary for JSON-LD. Its vocabulary can also be embedded in HTML documents using microdata or RDFa.
{
"@context": "https://schema.org",
"@type": "Person",
"name": "Jane Doe",
"jobTitle": "Professor",
"telephone": "(425) 123-4567",
"url": "http://www.janedoe.com"
}It is RECOMMENDED to reuse existing vocabularies, such as Schema.org, wherever there is a good fit for an application’s schema. Even if a vocabulary does not cover all the concepts required by the application, it is still better to try to reuse existing vocabularies, even if only partially. This saves time and effort designing new schemas, and it also helps to keep data as interoperable as possible with other systems. Even if interoperability is not a requirement now, it may be in the future.
For example, Schema.org’s Person type, which defines
properties such as givenName, familyName, jobTitle, and telephone, is a
good basis from which to design a schema for users, customers, or other such
entities.
For further guidance on using JSON-LD, the W3C maintains a JSON-LD Best Practices document.
JSON Pointer
IETF RFC 6901 defines a syntax
for identifying a specific value in a JSON document. A JSON Pointer is a string
beginning with a forward slash /, with each subsequent slash separating path
segments that identify nested objects and arrays:
/path/to/property
Elements of arrays can be referenced using zero-indexed numeric indices:
/users/0/name
An empty string "" refers to an entire document.
There are a couple of special characters. The / character in a property name
is escaped as ~1. The ~ character is escaped as ~0. It is RECOMMENDED to
not include these, or any other special characters, in property names, to make
traversal of JSON data structures as easy as possible for all clients.
JSON Pointer is used for data extraction, validation, and transformation. It is also used for partial updates via JSON Patch operations (see JSON Patch). Relevant to this technical standard, JSON Pointer syntax is RECOMMENDED for creating cross-references within JSON documents. Example:
{
"categories": [
{
"id": "electronics",
"name": "Electronics"
},
{
"id": "computers",
"name": "Computers",
"parentCategory": "/categories/0"
},
{
"id": "laptops",
"name": "Laptops",
"parentCategory": "/categories/1"
}
],
"products": [
{
"id": "laptop1",
"name": "UltraBook Pro",
"price": 1299.99,
"categoryRef": "/categories/2",
"relatedProducts": ["/products/1"]
},
{
"id": "laptop2",
"name": "DevBook Max",
"price": 1499.99,
"categoryRef": "/categories/2",
"relatedProducts": ["/products/0"]
}
]
}JSON Patch
IETF RFC 6902 defines JSON
Patch, a format for describing a sequence of changes to apply to a JSON
document, addressed by JSON Pointer. A patch document is a JSON array of
operation objects, each with an op member naming the operation and a
path member giving its JSON Pointer target:
[
{ "op": "replace", "path": "/products/0/price", "value": 1199.99 },
{ "op": "add", "path": "/products/-", "value": { "id": "laptop3" } },
{ "op": "remove", "path": "/products/1/relatedProducts/0" },
{ "op": "move", "from": "/categories/2", "path": "/categories/0" },
{ "op": "copy", "from": "/products/0", "path": "/products/-" },
{ "op": "test", "path": "/products/0/name", "value": "UltraBook Pro" }
]The six operations are add, remove, replace, move, copy, and
test. add, remove, and replace take a path (and, except for
remove, a value); move and copy take a from source pointer in
addition to their path destination; test asserts that path currently
holds value, failing the whole patch if it does not — useful as an
optimistic-concurrency guard immediately before a replace. A - as the
final path segment of an array target, as in the add and copy operations
above, appends to the end of the array rather than addressing an existing
index.
It is RECOMMENDED to use JSON Patch for partial-update APIs (PATCH
requests) over an ad-hoc partial-document format, since it is a standard
that client and server tooling both already support, and test gives
callers a built-in guard against lost updates that a bespoke format would
have to reinvent.
JSON Type Definition (JTD)
JSON Type Definition (JTD) is an alternative schema language for JSON documents. It was created in response to JSON Schema and is designed to be simpler, focusing on structural and type validation, with a lighter weight vocabulary and fewer constraints.
{
"properties": {
"name": { "type": "string" },
"age": { "type": "uint8" },
"email": { "type": "string" },
"isSubscribed": { "type": "boolean" },
"registrationDate": { "type": "timestamp" }
},
"optionalProperties": {
"phoneNumber": { "type": "string" },
"address": {
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"country": { "type": "string" }
},
"optionalProperties": {
"postalCode": { "type": "string" }
}
},
"tags": {
"elements": {
"type": "string"
}
}
}
}JSON Schema is the de facto standard for JSON schema, and it is RECOMMENDED for its expressive power and readily-available libraries and tooling. However, JTD is a good alternative for simple use cases, especially where type safety is the primary concern (rather than full schema validation), and it is mentioned here for completeness.
Schema forms
Every JTD schema is one of eight forms, distinguished by which keyword it
uses: empty ({}, matches any instance), type (a primitive), enum, an
elements array, properties/optionalProperties (an object), values
(a map with a uniform value type), discriminator (a tagged union), or
ref (a reference into definitions). A schema MUST use exactly one of
these keywords (aside from nullable and metadata, below); combining two
forms, such as properties and elements, in the same schema object is
invalid.
The properties form defaults to rejecting any instance property not
explicitly listed — the equivalent of JSON Schema’s
additionalProperties: false (see Applicator keywords) — with no
per-schema way to opt out. This is a sharper default than JSON Schema’s,
where additionalProperties defaults to permissive.
nullable and metadata
Every JTD form accepts two further members alongside its form-defining
keyword: nullable, a boolean that, if true, permits the instance to be
null in addition to the form’s own type; and metadata, a
free-form object for documentation and tooling — the JTD analogue of
$comment and the annotation keywords (see Schema identification and
Annotations), with the same non-enforcing status.
definitions and referencing
The ref form and its accompanying root-level definitions dictionary are
JTD’s equivalent of $ref/$defs (see Schema identification), but
narrower: ref MUST target an entry in definitions and cannot reference a
non-root subschema, and definitions cannot reference another JSON
document — there is no JTD equivalent of a cross-file $ref (see
Cross-references). A JTD schema that needs to share structure across
files has no direct mechanism for it, unlike JSON Schema.
Migrating from JSON Schema
Because JTD’s vocabulary is a deliberate subset, migrating a JSON Schema
document to JTD is lossy wherever the source schema uses a keyword with no
JTD equivalent — pattern, numeric range constraints, oneOf (JTD’s
discriminator form covers only the tagged-union case, not general
exclusive-or composition), or $dynamicRef (see
Schema identification) all have no JTD counterpart. It is RECOMMENDED to
migrate to JTD only where the source schema’s constraints already fit
within JTD’s eight forms, and to stay on JSON Schema otherwise rather than
silently dropping constraints in the conversion.
Schema versioning
The versioning discussed in Versions is the JSON Schema draft a document is written against. This section is about versioning a schema of your own design as the data model it describes changes over time — a concern of every long-lived API or data store, independent of which draft is in use.
It is RECOMMENDED to version a schema explicitly, rather than editing it in place, wherever the schema describes data that outlives a single deployment — a network API payload, a persisted document, or a message on a queue. Editing a published schema in place is a breaking change for every consumer that validated against the old shape, even where the edit looks additive.
Backward-compatible changes
A change is backward-compatible, and MAY be made in place without a new schema version, only where every instance that validated against the old schema still validates against the new one. This includes:
- Adding a new OPTIONAL property (not listed in
required). - Widening a constraint — for example, raising
maxLength, or adding a value to anenum. - Adding a new, non-exclusive branch to
anyOf(but notoneOf, where adding a branch can turn a previously-unique match into an ambiguous one).
Breaking changes
A change is breaking wherever an instance that validated against the old
schema could fail the new one — removing a property, adding it to
required, narrowing a constraint, or changing a type. It is RECOMMENDED
to publish a breaking change as a new schema at a new $id
(see Schema identification), for example
https://example.com/schemas/order/v2/order.schema.json, rather than
mutating the existing document at its existing $id. This lets a consumer
pin to the schema version it was built against, and migrate deliberately
rather than being broken by an unannounced change at a stable URL.
It is RECOMMENDED to encode the schema’s own version as a top-level
property of the data it describes (for example, a schemaVersion or
version field), independent of the $id version, wherever the data is
persisted or transmitted without the schema alongside it — a stored
document, or a message on a queue read long after it was written. The $id
version identifies which schema document validates the data; the data’s own
version field lets code that reads the data later determine which shape to
expect without first locating and fetching that schema document.
Best practices
The following practices support consistency across a team or organization’s schemas, beyond the correctness of any single schema.
Centralize shared schemas
It is RECOMMENDED to maintain shared subschemas — common structures such as
an address, a monetary amount, or a pagination envelope — in a central
repository, referenced by $ref (see Cross-references) from each
schema that uses them, rather than letting each team or service redefine
its own copy. A redefined copy drifts: two "address" schemas that started
identical diverge silently as each is edited independently, and a consumer
integrating with both then has to reconcile two shapes that were meant to
be the same.
Validate in CI/CD
It is RECOMMENDED to validate schemas — both that each schema document is itself well-formed against its metaschema, and that a corpus of example or recorded payloads still validates against it — as an automated CI/CD pipeline step, rather than relying on manual review. This catches a malformed schema or an inadvertent breaking change (see Schema versioning) before it reaches consumers, using the same validator library a consumer’s own runtime would use (see Validator libraries).
Naming conventions
It is RECOMMENDED to pick one property-naming convention — camelCase,
snake_case, or kebab-case — and apply it consistently across all of an
organization’s schemas, matching the convention already used by the target
language or platform’s own idioms where one schema is consumed
predominantly by a single ecosystem (for example, camelCase for a schema
consumed mainly by JavaScript clients). Mixing conventions within one
schema, or between schemas that a single consumer must integrate with
together, forces that consumer to special-case each shape rather than
applying one mapping.
It is RECOMMENDED to use plural property names for array-valued properties
(items, not item) and singular names for everything else, so that a
property’s cardinality is legible from its name alone, without inspecting
its type.
Consistent data typing
It is RECOMMENDED to keep every element of an array-valued property
homogeneously typed — enforced with items (see Applicator keywords)
naming a single subschema, or prefixItems where positions genuinely
differ in shape. An array whose elements vary in type (for example, mixing
strings and objects) forces every consumer to branch on each element’s
runtime type before it can be processed, defeating the purpose of
declaring a schema in the first place.
Limit nesting depth
It is RECOMMENDED to prefer a flatter, normalized model — sibling schemas
linked by an identifying key and composed with $ref (see
Cross-references and Schema identification) — over deeply nested
subschemas, wherever the nested data is also meaningful as a
first-class entity in its own right, such as a customer nested inside an
order rather than referenced by customerId. Deep nesting duplicates the
nested entity’s own schema at every point it is embedded, and complicates
partial updates (see JSON Patch), whose paths grow with the nesting
depth. Nesting is still appropriate for data that is never meaningful on
its own, such as an address that only ever exists as part of the entity
that owns it.
Document data models
title and description (see Annotations) document a schema’s
individual keywords, but they do not, on their own, explain the model —
why a property exists, the relationships between properties, or a
property’s provenance. It is RECOMMENDED to accompany a non-trivial schema
with prose documentation, alongside the schema document rather than
embedded within it, covering the model’s purpose and relationships; a
$comment (see Schema identification) is appropriate for a narrower,
implementation-facing note, but is not a substitute for that
documentation, since $comment is per-keyword and tooling MAY strip it
before it reaches a reader.
Modeling patterns
The following patterns recur across schemas for common data shapes.
Monetary values
It is RECOMMENDED to model a monetary amount as a string, not a number:
{
"type": "object",
"properties": {
"currency": { "type": "string", "pattern": "^[A-Z]{3}$" },
"value": { "type": "string", "pattern": "^-?[0-9]+\\.[0-9]{2,}$" }
},
"required": ["currency", "value"]
}JSON’s number type is a floating-point value in every mainstream
implementation, and floating-point arithmetic cannot represent every decimal
fraction exactly — 0.1 + 0.2 does not equal 0.3 in IEEE 754 binary
floating point. A monetary value that has passed through even one arithmetic
operation as a JSON number risks losing precision at the subdivision (for
example, cent) level. A string, decoded by the consumer into a
language-native decimal or fixed-point type rather than a native float,
avoids this entirely. Pair value with a currency property (an ISO 4217
code) rather than assuming a fixed currency, since a subdivision’s size
varies by currency.
Phone numbers
It is RECOMMENDED to model a phone number as a structured object, not a single free-form string, wherever the number must be validated or acted on (for example, redialed) rather than merely stored and displayed:
{
"type": "object",
"properties": {
"countryCode": { "type": "string", "pattern": "^[1-9][0-9]{0,2}$" },
"nationalNumber": { "type": "string", "pattern": "^[0-9]{4,14}$" },
"extension": { "type": "string", "pattern": "^[0-9]{1,7}$" }
},
"required": ["countryCode", "nationalNumber"]
}This follows the structure of ITU-T E.164, the international public telecommunication numbering plan. A single free-form string is RECOMMENDED only where the number is opaque to the schema’s consumers — for example, a contact field that is only ever displayed, never dialed or validated — since round-tripping a structured number back to a single string for display is a one-line concern, but recovering the structure from an unconstrained string is not.
Vendor-prefixed format values
format (see Format) is not limited to its predefined values — email,
date-time, and the rest. It is RECOMMENDED to define a custom,
vendor-prefixed format string, such as acme_currency_code_v1, to signal
that a string property carries a semantic validation rule beyond what
base JSON Schema’s pattern and enum keywords can express concisely, or
that is enforced by a shared internal library rather than the schema itself.
The prefix and version suffix distinguish it from any future
officially-registered format value, and let the semantic rule itself
evolve (a _v2) without changing the property’s declared type. Since
format is annotation-only by default (see Format), a vendor-prefixed
value has no effect on validation unless the consuming validator is
explicitly configured to enforce it — document that expectation alongside
the schema.
Error and envelope schemas
It is RECOMMENDED to define one shared error schema, referenced by $ref
(see Cross-references) from every endpoint or message type that can
fail, rather than letting each one define its own error shape:
{
"type": "object",
"properties": {
"code": { "type": "string" },
"message": { "type": "string" },
"details": {
"type": "array",
"items": {
"type": "object",
"properties": {
"field": { "type": "string" },
"issue": { "type": "string" }
},
"required": ["field", "issue"]
}
},
"links": {
"type": "object",
"additionalProperties": { "type": "string", "format": "uri" }
}
},
"required": ["code", "message"]
}code and message are RECOMMENDED at minimum — a stable, machine-readable
identifier and a human-readable summary. details is RECOMMENDED wherever
the error can originate from more than one field, such as a validation
failure, so a consumer can report each failing field individually rather
than parsing a combined message string. links follows the HATEOAS
convention of naming next-step URIs (for example, a documentation link
explaining the error code) directly in the payload, and is OPTIONAL —
appropriate where consumers benefit from discovering related resources at
runtime, and skippable where the API’s error codes are already documented
out-of-band.
References
- Learn JSON Schema (n.d.). 2020-12 keyword reference. — The source for the Validation, Applicator, Core, Unevaluated, Meta-Data (Annotations), and Content vocabulary keyword definitions in Validation keywords, Applicator keywords, Schema identification, Unevaluated keywords, Annotations, and Content keywords.
- JSON Schema (n.d.).
Applying Subschemas Conditionally.
— The source for the
if/then/elseanddependentRequired/dependentSchemasguidance in Conditional validation. - JSON Schema (n.d.).
Format.
— The source for the annotation-vs-assertion distinction for
formatin Format. - JSON Schema (n.d.).
Modular JSON Schema Combination.
— The source for the
$defs,$anchor,$dynamicAnchor/$dynamicRef, and recursive-schema guidance in Schema identification. - Devzery (n.d.).
Your Ultimate Guide to Schema for JSON.
— The source for the document-skeleton guidance (
$schema,$id,title,description) in Schema identification. - IETF (2013). RFC 6902. JavaScript Object Notation (JSON) Patch. — The source for the operation model in JSON Patch.
- Ajv (n.d.). Ajv JSON Schema Validator. — The source
for the
ajventry in Validator libraries. - Ajv (n.d.).
JSON Type Definition. —
The source for the schema forms,
nullable/metadata,definitions, and migration guidance in JSON Type Definition (JTD). - Devzery (n.d.). Your Ultimate Guide to Schema for JSON. — The source for Schema versioning.
- LinkedIn (n.d.). How Do You Keep Your JSON Data Models Consistent?. — The source for Best practices.
- PayPal (n.d.).
PayPal API Standards — JSON Schema examples.
— The source for the monetary-value, phone-number, vendor-prefixed
format, file-organization, and error/envelope patterns in Modeling patterns and Cross-references. - ITU-T (2010). E.164: The International Public Telecommunication Numbering Plan. — The source for the phone-number structure in Modeling patterns.
Useful links
- Hyperjump — Free-to-use online JSON Schema validation tool.
- JSON Schema Store — A repository of JSON Schema for commonly-used JSON configuration files such as package.json.
Validator libraries
A schema is only as useful as the validator enforcing it. It is RECOMMENDED to validate against a schema using an established library for the target language, rather than hand-rolling validation logic:
- ajv — JavaScript/TypeScript. The most widely used validator in the Node.js ecosystem; supports Draft 2020-12 and JTD (see JSON Type Definition (JTD)).
- jsonschema — Python. The reference implementation used by most Python projects that validate against JSON Schema.
- santhosh-tekuri/jsonschema — Go. A dependency-free validator supporting Draft 2020-12.
joi and zod are NOT RECOMMENDED as substitutes for a JSON Schema
validator: both define their own, JSON-Schema-incompatible validation DSL,
so a schema written for one cannot be shared with a JSON Schema tool such as
an OpenAPI generator (see OpenAPI) or ajv. Use them only where the
validated data never needs to be described as a portable JSON Schema
document.
Each library exposes its own language-native API for invoking validation
and handling a failure — for example, Python’s jsonschema offers a
validate() function that raises a ValidationError. That API is
implementation detail specific to the library and the language, and is
outside this standard’s scope; consult the library’s own documentation.