DevToys Web Pro iconDevToys Web Proብሎግ
ተተርጉሟል በ LocalePack logoLocalePack
ደረጃ ይስጡን፦
የአሳሽ ቅጥያን ይሞክሩ፦
← Back to Blog

JSON Schema Validation Guide: Draft Versions, Keywords, and CI Integration

11 min read

JSON Schema is the standard way to describe the shape of JSON data — what fields exist, which are required, what types they hold, and what constraints they satisfy. Use it for API contracts, configuration file validation, code generation, and automated testing. Follow along with the JSON Schema Validator to validate schemas and payloads as you read.

Why Use JSON Schema

A schema serves four distinct purposes simultaneously, which is why it has become the backbone of modern API tooling:

  • Runtime validation: reject malformed requests before they touch business logic. A single schema check at the API boundary eliminates entire classes of null pointer errors and unexpected type coercions.
  • Documentation: a schema is machine-readable documentation. Tools like Swagger UI and Redoc render it as interactive API references automatically.
  • Type generation: tools such as json-schema-to-typescript, QuickType, and Pydantic can generate typed models directly from a schema, keeping runtime validation and static types in sync. See also the JSON to TypeScript guide.
  • API contracts: schemas form the contract between producer and consumer. Publishing a schema and validating against it in CI means breaking changes are caught before they reach production.

A Minimal Schema Example

Before diving into draft versions and advanced keywords, here is a working schema that describes a user object:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "id":    { "type": "integer" },
    "name":  { "type": "string", "minLength": 1 },
    "email": { "type": "string", "format": "email" },
    "role":  { "type": "string", "enum": ["admin", "editor", "viewer"] }
  },
  "required": ["id", "name", "email"],
  "additionalProperties": false
}

This schema accepts an object with id, name, and email as required fields, an optional role constrained to three values, and rejects any property not listed. Copy it into the JSON Schema Validator to see exactly which payloads pass and which fail.

Draft Versions

JSON Schema has evolved through several published drafts. The version is declared with $schema. Knowing which draft your tooling implements prevents subtle validation surprises.

DraftYearStatusNotable changes
draft-042013LegacyFirst widely adopted version; introduced $ref
draft-062017LegacyAdded $id, contains, const, propertyNames
draft-072018Most usedAdded if/then/else, readOnly, writeOnly, $comment
2019-092019TransitionalRenamed definitions to $defs; $recursiveRef; unevaluatedProperties
2020-122020LatestprefixItems replaces tuple-form items; $dynamicRef; OAS 3.1 compatible

Draft-07 remains the most widely supported across validators and code generators. Use 2020-12 if you are writing OpenAPI 3.1 specs or if your toolchain explicitly supports it. The most visible breaking change between drafts is array validation: in draft-07, a tuple is "items": [{...}, {...}] (an array of schemas); in 2020-12 that becomes "prefixItems", and "items" now only applies to additional items.

Core Keywords

These keywords appear in almost every real-world schema. Understanding each one precisely prevents validation surprises.

  • type — restricts the JSON type: "string", "number", "integer", "boolean", "object", "array", "null". A value can match multiple types: "type": ["string", "null"] is a nullable string.
  • properties — defines schemas for named object properties. A property listed here is not required unless also in required.
  • required — array of property names that must be present. An empty object {} satisfies required: [].
  • additionalProperties — when false, any property not in properties or patternProperties causes validation to fail. When a schema object, additional properties must match that schema.
  • patternProperties — like properties but keys are regex patterns. Useful for maps with dynamic keys: "^[a-z]+$": { "type": "number" }.
  • enum — restricts the value to one of a fixed list. "enum": ["active", "inactive"] rejects anything else.
  • const — restricts the value to exactly one literal. "const": "v2" is equivalent to "enum": ["v2"].

Array Validation

Arrays have their own set of keywords. In draft-07, array validation looks like this:

{
  "type": "array",
  "items": { "type": "string" },
  "minItems": 1,
  "maxItems": 100,
  "uniqueItems": true,
  "contains": { "const": "admin" }
}
  • items (draft-07) — schema that every element must satisfy. In 2020-12 this keyword only applies to items after any prefixItems.
  • prefixItems (2020-12) — array of schemas for positional tuple elements.
  • contains — at least one element must match. Combined with minContains and maxContains (2019-09+) for range constraints.
  • minItems / maxItems — length bounds.
  • uniqueItems — when true, all elements must be distinct. Deep equality is used for objects.

$ref and $defs

Real schemas quickly develop repeated sub-schemas — an address object used in billing, shipping, and contact fields. $defs (called definitions in draft-07) lets you define reusable schemas in one place and reference them with $ref.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$defs": {
    "Address": {
      "type": "object",
      "properties": {
        "street": { "type": "string" },
        "city":   { "type": "string" },
        "zip":    { "type": "string", "pattern": "^[0-9]{5}$" }
      },
      "required": ["street", "city", "zip"]
    }
  },
  "type": "object",
  "properties": {
    "billing":  { "$ref": "#/$defs/Address" },
    "shipping": { "$ref": "#/$defs/Address" }
  }
}

$ref accepts a JSON Pointer within the same document (#/$defs/Name) or a URL to an external schema file ("$ref": "./address.schema.json"). For recursive structures such as a tree node that contains child nodes of the same type, the self-referencing $ref is the only practical option:

{
  "$defs": {
    "TreeNode": {
      "type": "object",
      "properties": {
        "value":    { "type": "string" },
        "children": {
          "type": "array",
          "items": { "$ref": "#/$defs/TreeNode" }
        }
      },
      "required": ["value"]
    }
  },
  "$ref": "#/$defs/TreeNode"
}

Conditional Validation

Many real payloads have fields that are required only under certain conditions. JSON Schema provides several mechanisms.

if/then/else (draft-07+): when the if sub-schema passes, the payload must also satisfy then; when it fails, it must satisfy else.

{
  "if":   { "properties": { "type": { "const": "company" } } },
  "then": { "required": ["vatNumber"] },
  "else": { "required": ["personalId"] }
}

allOf / anyOf / oneOf: combine multiple sub-schemas.

  • allOf — payload must satisfy every sub-schema. Used to extend a base schema with additional constraints.
  • anyOf — payload must satisfy at least one sub-schema. Useful for union types.
  • oneOf — payload must satisfy exactly one sub-schema. Useful for discriminated unions where payloads are mutually exclusive.
{
  "oneOf": [
    { "$ref": "#/$defs/CreditCardPayment" },
    { "$ref": "#/$defs/BankTransferPayment" },
    { "$ref": "#/$defs/CryptoPayment" }
  ]
}

Tooling Comparison

ToolLanguageDraftsStrengths
ajvJavaScript / TypeScriptdraft-04 through 2020-12Fastest JS validator; JIT-compiles schemas; best for Node.js APIs and CI
zodTypeScriptSchema-builder (TypeScript-first)TypeScript inference without codegen; composable; great DX in Next.js and tRPC stacks
valibotTypeScriptSchema-builder (TypeScript-first)Tree-shakeable bundle — only pays for what you use; smaller bundle impact than zod
PydanticPythonGenerates draft-07 / 2020-12Python model validation; used by FastAPI; can export JSON Schema for cross-language use
jsonschemaPythondraft-03 through 2020-12Reference implementation for Python; good for one-off validation scripts

For a JavaScript project validating JSON Schema documents directly, ajv is the standard choice. For TypeScript projects where you author schemas in code rather than JSON, zod or valibot provide better ergonomics. They can also export JSON Schema via adapter packages (zod-to-json-schema, @valibot/to-json-schema) when you need a portable schema artifact.

// ajv — validate against a JSON Schema document
import Ajv from 'ajv';
import addFormats from 'ajv-formats';

const ajv = new Ajv();
addFormats(ajv);

const schema = {
  type: 'object',
  properties: {
    name:  { type: 'string' },
    email: { type: 'string', format: 'email' },
  },
  required: ['name', 'email'],
};

const validate = ajv.compile(schema);
const valid = validate({ name: 'Alice', email: 'alice@example.com' });
if (!valid) console.error(validate.errors);

OpenAPI and JSON Schema

OpenAPI Specification (OAS) uses JSON Schema to describe request and response bodies. The relationship has changed across OAS versions:

  • OAS 2.0 (Swagger): used a proprietary subset of JSON Schema draft-04. Keywords like nullable were OAS extensions, not standard JSON Schema.
  • OAS 3.0: used a draft-07-inspired dialect but with modifications — nullable: true instead of type: ["string", "null"], and no $ref sibling keywords.
  • OAS 3.1: fully aligned with JSON Schema 2020-12. The dialect is declared with jsonSchemaDialect. You can now use type: ["string", "null"], sibling keywords alongside $ref, and all 2020-12 features.

If you are starting a new API, use OAS 3.1. If you are maintaining an OAS 3.0 spec and need to validate payloads against it, use ajv with the ajv-openapi dialect plugin rather than a plain JSON Schema validator. See also the API debug workflow guide for end-to-end request/response testing.

CI Validation Workflow

Schemas become most valuable when validation runs automatically on every pull request. The typical CI workflow has two layers: fixture validation and breaking-change detection.

1. Validate request/response fixtures against the schema:

// validate-fixtures.mjs
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import { readFileSync, readdirSync } from 'fs';

const ajv = new Ajv({ allErrors: true });
addFormats(ajv);

const schema = JSON.parse(readFileSync('./schemas/user.schema.json', 'utf8'));
const validate = ajv.compile(schema);

const fixturesDir = './test/fixtures/users';
let failed = 0;

for (const file of readdirSync(fixturesDir)) {
  const data = JSON.parse(readFileSync(`${fixturesDir}/${file}`, 'utf8'));
  if (!validate(data)) {
    console.error(`FAIL ${file}:`, validate.errors);
    failed++;
  }
}

if (failed > 0) process.exit(1);
console.log('All fixtures valid.');

2. Detect breaking schema changes between branches:

# Install json-schema-diff
npm install -g json-schema-diff

# Compare the current schema against main
json-schema-diff schemas/user.schema.json@main schemas/user.schema.json

# Fail CI if any removal or type narrowing is detected
# (additions are backward-compatible; removals are breaking)

Add these steps to a GitHub Actions workflow to catch both malformed fixtures and breaking contract changes:

# .github/workflows/schema-validation.yml
name: Schema Validation
on: [pull_request]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: node validate-fixtures.mjs
      - run: |
          npm install -g json-schema-diff
          git show origin/main:schemas/user.schema.json > /tmp/schema-main.json
          json-schema-diff /tmp/schema-main.json schemas/user.schema.json

This two-step approach catches two distinct failure modes: a fixture that was never updated after a schema tightening, and a schema change that silently removes a field consumers depend on.


Validate schemas and payloads interactively with the JSON Schema Validator. For related topics, see the JSON formatter guide, the JSON to TypeScript types guide, and the API debug workflow guide.