Cheatsheet

JSON Schema, from the basics to the parts nobody explains.

Every keyword you reach for, ordered from beginner to advanced, with copyable recipes for conditionals, composition, and dynamic references. Jump ahead with the on-this-page menu, or filter to find a keyword fast.

Beginner

The shape of a schema and the everyday constraints you reach for first.

Core

Core keywords

What you'll learn: how a schema is structured, how to declare the dialect, and the handful of keywords you'll use in almost every schema.

$schema
Declare the dialect at the root of a schema so validators know which rules apply. Rusl schemas use draft 2020-12.
{
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
type
Constrain the JSON type: string, number, integer, boolean, object, array, or null. Use an array to allow more than one, such as a value that may be a string or null.
{
  "type": ["string", "null"]
}
enum / const
enum lists the allowed values; const fixes a single value. Prefer const over a one-item enum — it reads clearer and produces a better error message.
{
  "status": { "enum": ["active", "archived"] },
  "kind": { "const": "rusl/schema" }
}
title / description
Human- and agent-readable labels. They don't affect validation, but they're what tools and people read to understand what a field means.
Strings & numbers

Strings and numbers

What you'll learn: the constraints that bound text and numeric values — lengths, patterns, named formats, ranges, and steps.

minLength / maxLength
Bound the length of a string, measured in characters.
{
  "type": "string",
  "minLength": 1,
  "maxLength": 280
}
pattern
Require the string to match a regular expression. It's a partial match unless you anchor it with ^ and $.
{
  "type": "string",
  "pattern": "^[a-z][a-z0-9-]*$"
}
format
Tag a string with a known format like email, uri, uuid, or date-time. Many validators treat format as a hint unless assertion is turned on.
{
  "type": "string",
  "format": "email"
}
minimum / maximum
Inclusive numeric bounds: the value must be at least minimum and at most maximum.
{
  "type": "integer",
  "minimum": 0,
  "maximum": 100
}
exclusiveMinimum / exclusiveMaximum
Exclusive numeric bounds. In 2020-12 these take a number, not a boolean — the value must be strictly greater or less than it.
{
  "type": "number",
  "exclusiveMinimum": 0
}
multipleOf
Require the number to be a multiple of a step, such as 0.01 for currency or 5 for round increments.
{
  "type": "number",
  "multipleOf": 0.01
}
Objects

Objects

What you'll learn: how to describe an object's shape — which properties it has, which are required, and what to do with everything else.

properties
Describe each named property with its own schema. A property stays optional until you also list it in required.
{
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "age": { "type": "integer" }
  }
}
required
List the property names that must be present. Note: a property that is present but null still satisfies required — null is not the same as absent.
{
  "type": "object",
  "properties": {
    "id": { "type": "string" }
  },
  "required": ["id"]
}
additionalProperties
Control properties not named above. Set it to false to forbid extras — but it can't see properties contributed by allOf, $ref, or if (see Gotchas).
{
  "type": "object",
  "properties": {
    "id": { "type": "string" }
  },
  "additionalProperties": false
}
patternProperties
Validate properties whose names match a regular expression — useful for maps with dynamic keys.
{
  "type": "object",
  "patternProperties": {
    "^x-": { "type": "string" }
  }
}
propertyNames
Constrain the keys themselves rather than their values — for example, require every key to be a lowercase slug.
{
  "type": "object",
  "propertyNames": {
    "pattern": "^[a-z_]+$"
  }
}
Arrays

Arrays

What you'll learn: how to validate lists, fixed-shape tuples, and "must contain" rules.

items
Apply one schema to every element. In 2020-12, items is the per-element schema — the old tuple form moved to prefixItems.
{
  "type": "array",
  "items": { "type": "string" }
}
prefixItems
Validate elements by position (a tuple). Pair it with items: false to forbid anything beyond the listed positions.
{
  "type": "array",
  "prefixItems": [
    { "type": "number" },
    { "type": "string" }
  ],
  "items": false
}
minItems / maxItems
Bound how many elements the array may contain.
{
  "type": "array",
  "minItems": 1,
  "maxItems": 10
}
uniqueItems
Set to true to require every element to be distinct — a set rather than a list.
{
  "type": "array",
  "uniqueItems": true
}
contains + minContains / maxContains
Require at least (or at most) N elements to match a schema, without constraining the others.
{
  "type": "array",
  "contains": { "type": "number" },
  "minContains": 2
}
Intermediate

Composition and conditional logic: schemas that react to the data they validate.

Combining

Combining schemas

What you'll learn: how to build a schema out of other schemas with boolean logic — the foundation for reuse and variants.

allOf
The value must satisfy every subschema. Use it to extend one schema with extra constraints.
{
  "allOf": [
    { "type": "object", "properties": { "id": { "type": "string" } } },
    { "required": ["id"] }
  ]
}
anyOf
The value must satisfy at least one subschema. Good for "one of several acceptable shapes".
{
  "anyOf": [
    { "type": "string" },
    { "type": "number" }
  ]
}
oneOf
The value must satisfy exactly one subschema. Use it for mutually exclusive variants — every branch is evaluated, so keep them genuinely exclusive.
{
  "oneOf": [
    { "type": "string", "maxLength": 5 },
    { "type": "number" }
  ]
}
not
The value must not satisfy the subschema. Often used to exclude a specific value or type.
{
  "not": { "type": "null" }
}
Conditionals

Conditionals and dependencies

What you'll learn: rules that react to the data itself — how to require or constrain a field only in certain cases.

if / then / else
Plain conditional logic: when the data matches if, it must also match then; otherwise it must match else (else is optional). One catch — also list the checked field under required inside if, or data that simply omits the field still "matches" and the rule fires anyway.
{
  "type": "object",
  "properties": {
    "country": { "type": "string" },
    "postcode": { "type": "string" }
  },
  "if": {
    "properties": { "country": { "const": "US" } },
    "required": ["country"]
  },
  "then": { "required": ["postcode"] }
}
dependentRequired
If a property is present, require others — by presence alone, without inspecting the value.
{
  "type": "object",
  "dependentRequired": {
    "credit_card": ["billing_address"]
  }
}
dependentSchemas
If a given property is present, the whole object must also match an extra schema. Here, the moment credit_card appears, billing_address becomes required and payment_method must be "card". It's the more powerful sibling of dependentRequired: same trigger (a property being present), but the result is a full schema instead of just a list of required fields.
{
  "type": "object",
  "properties": {
    "payment_method": { "type": "string" }
  },
  "dependentSchemas": {
    "credit_card": {
      "required": ["billing_address"],
      "properties": {
        "billing_address": { "type": "string" },
        "payment_method": { "const": "card" }
      }
    }
  }
}
Referencing

Referencing and reuse

What you'll learn: how to define a schema once and reuse it — including how to drop one sub-schema into another with $ref.

$defs
The standard place to keep reusable subschemas. Define once here, reference anywhere with $ref.
{
  "$defs": {
    "uuid": { "type": "string", "format": "uuid" }
  },
  "type": "object",
  "properties": {
    "id": { "$ref": "#/$defs/uuid" }
  }
}
$ref
Pull in another schema by reference — a local definition like #/$defs/uuid, or an entire schema published on Rusl by its resource URL (resources.rusl.com/resources/account/schemas/schema). In 2020-12, keywords sitting next to $ref also apply, unlike draft-07 where they were ignored.
{
  "type": "object",
  "properties": {
    "id": { "$ref": "#/$defs/uuid" },
    "request": {
      "$ref": "https://resources.rusl.com/resources/rusl/schemas/context-request"
    }
  },
  "$defs": {
    "uuid": { "type": "string", "format": "uuid" }
  }
}
$id
Give a schema its own base URI. Relative $refs resolve against it, and it's how schemas reference each other across files.
{
  "$id": "https://resources.rusl.com/resources/rusl/schemas/common",
  "type": "object",
  "properties": {
    "email": { "type": "string", "format": "email" }
  }
}
$anchor
Name a location inside a schema so you can reference it as #name instead of a long JSON Pointer.
{
  "type": "object",
  "properties": {
    "billing": { "$ref": "#address" }
  },
  "$defs": {
    "address": {
      "$anchor": "address",
      "type": "object"
    }
  }
}
Advanced

The powerful, lesser-known keywords, plus recipes and the gotchas they exist to fix.

Advanced

Advanced keywords

What you'll learn: the keywords that make 2020-12 strictly more powerful — closing objects after composition, recursive references, and validating content carried inside strings.

unevaluatedProperties
The right way to forbid extra properties after allOf, $ref, if, or dependentSchemas. Unlike additionalProperties, it sees the properties those applied subschemas already allowed.
{
  "allOf": [{ "$ref": "#/$defs/base" }],
  "properties": {
    "extra": { "type": "string" }
  },
  "unevaluatedProperties": false,
  "$defs": {
    "base": {
      "type": "object",
      "properties": { "id": { "type": "string" } }
    }
  }
}
unevaluatedItems
The array counterpart: forbid extra elements once prefixItems or composition has accounted for some. It sees the positions already validated.
{
  "allOf": [{ "prefixItems": [{ "type": "number" }] }],
  "unevaluatedItems": false
}
$dynamicRef / $dynamicAnchor
References resolved at runtime against the dynamic scope — the tool for recursive types that a schema extending yours can specialize. $dynamicAnchor marks the target; $dynamicRef points at it.
{
  "$id": "https://resources.rusl.com/resources/rusl/schemas/tree",
  "$ref": "#/$defs/node",
  "$defs": {
    "node": {
      "$dynamicAnchor": "node",
      "type": "object",
      "properties": {
        "value": { "type": "string" },
        "children": {
          "type": "array",
          "items": { "$dynamicRef": "#node" }
        }
      }
    }
  }
}
contentEncoding / contentMediaType / contentSchema
Describe data carried inside a string, such as base64 bytes or a JSON document embedded as text. Most validators treat these as annotations rather than hard assertions.
{
  "type": "string",
  "contentMediaType": "application/json",
  "contentSchema": {
    "type": "object",
    "required": ["id"]
  }
}
$vocabulary
Declared in a meta-schema to state which keyword vocabularies a dialect requires. You meet it when authoring a custom dialect, not in everyday schemas.
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://resources.rusl.com/resources/rusl/schemas/dialect",
  "$vocabulary": {
    "https://json-schema.org/draft/2020-12/vocab/core": true,
    "https://json-schema.org/draft/2020-12/vocab/validation": true
  }
}
Recipes

Recipes

What you'll learn: copy-paste solutions to real problems that aren't obvious from the keyword list alone.

Require a field only when another is present and not null
The trick is required plus not: { type: "null" } inside if — together they mean "present and not null," which a plain property check can't express.
{
  "type": "object",
  "properties": {
    "vehicleType": { "type": ["string", "null"] },
    "topSpeed": { "type": "number" }
  },
  "if": {
    "properties": {
      "vehicleType": { "not": { "type": "null" } }
    },
    "required": ["vehicleType"]
  },
  "then": { "required": ["topSpeed"] }
}
Discriminated (tagged) union
Put the discriminator in top-level properties, then pin it with const in each oneOf branch. Validators select the matching branch by the tag.
{
  "type": "object",
  "required": ["type"],
  "properties": {
    "type": { "enum": ["car", "truck"] }
  },
  "oneOf": [
    {
      "properties": {
        "type": { "const": "car" },
        "wheels": { "type": "integer" }
      },
      "required": ["wheels"]
    },
    {
      "properties": {
        "type": { "const": "truck" },
        "loadCapacity": { "type": "number" }
      },
      "required": ["loadCapacity"]
    }
  ]
}
Mutually exclusive fields — A or B, not both
Combine oneOf with not + required in each branch so exactly one of the keys may appear.
{
  "type": "object",
  "properties": {
    "personalId": { "type": "string" },
    "companyId": { "type": "string" }
  },
  "oneOf": [
    { "required": ["personalId"], "not": { "required": ["companyId"] } },
    { "required": ["companyId"], "not": { "required": ["personalId"] } }
  ]
}
Strictly close an object after allOf or if
Use unevaluatedProperties: false, never additionalProperties: false — only the former sees properties contributed by allOf, $ref, and if.
{
  "allOf": [{ "$ref": "#/$defs/base" }],
  "properties": {
    "premium": { "type": "boolean" }
  },
  "unevaluatedProperties": false,
  "$defs": {
    "base": {
      "type": "object",
      "properties": { "name": { "type": "string" } }
    }
  }
}
Recursive, self-referential structure
For a plain tree, point $ref at the node itself. Switch to $dynamicAnchor + $dynamicRef when a schema that extends yours should be able to specialize the recursive type.
{
  "$id": "https://resources.rusl.com/resources/rusl/schemas/category",
  "$ref": "#/$defs/node",
  "$defs": {
    "node": {
      "type": "object",
      "properties": {
        "name": { "type": "string" },
        "children": {
          "type": "array",
          "items": { "$ref": "#/$defs/node" }
        }
      }
    }
  }
}
Optional field that must be valid when present
Define it under properties and leave it out of required — absence is always allowed, but a present value must match. Add null to the type to also permit an explicit null.
{
  "type": "object",
  "properties": {
    "discountCode": {
      "type": ["string", "null"],
      "minLength": 6,
      "pattern": "^[A-Z0-9]+$"
    }
  }
}
Reuse a schema published on Rusl
Reference another account's schema by its resource URL, exactly as you would a local $def. This is how Rusl schemas build on each other instead of copying shapes.
{
  "type": "object",
  "properties": {
    "request": {
      "$ref": "https://resources.rusl.com/resources/rusl/schemas/context-request"
    }
  }
}
Gotchas

Common gotchas

What you'll learn: the mistakes almost everyone makes once.

if without required passes silently
Checking a property's value inside if isn't enough — a document that omits the property still matches if, so then fires when you didn't intend. Always add the property to required inside if.
additionalProperties can't see allOf or $ref
additionalProperties only knows the properties and patternProperties in the same schema object. After allOf, $ref, if, or dependentSchemas, reach for unevaluatedProperties instead.
null is not the same as absent
An explicit null still satisfies required and still triggers dependentRequired — the key is present. To mean "present and not null," pair the property with not: { type: "null" }.
$ref siblings now apply
In draft-07, keywords next to $ref were ignored. In 2020-12 they're evaluated alongside the reference, so a $ref with a sibling minLength now means both.
{
  "$ref": "#/$defs/name",
  "minLength": 3,
  "$defs": {
    "name": { "type": "string" }
  }
}
items changed meaning
The old array form of items (tuple validation) is now prefixItems. In 2020-12, items is the single schema applied to the remaining elements. Mixing drafts silently breaks tuples.
format may not be enforced
Many validators treat format as an annotation and won't reject bad values unless format assertion is enabled. If you need a hard guarantee, back it with pattern.