TS-29: JSON Schema
🚧 DRAFT
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
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.
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.