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

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
}
Example: a bounded, unique tag list

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]$" }
    }
  }
}
Example: a postal code format that depends on country

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"
  }
}
Example: a base64-encoded JSON payload

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"
    }
  }
}
Example

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

self

Identifies the resource itself.

next/prev

Indicates the next or previous resource in a sequence, eg. in a paginated list.

first/last

Refers to the first or last resource in a sequence.

up

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 templatePointers to clearly map data to template variables.
  • Use targetSchema to describe expected response formats.
  • Use targetHints for media types and methods.
  • Add title properties 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:

  • @context references a vocabulary that describes the concepts in the document.
  • @type indicates 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"
      }
    }
  }
}
Example

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 an enum.
  • Adding a new, non-exclusive branch to anyOf (but not oneOf, 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

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.