How do I validate JSON against a JSON Schema?

Dark navy cover card with the title 'How do I validate JSON against a JSON Schema?' in white and a green accent bar

To validate JSON against a JSON Schema, you write a schema that describes the shape you expect, declare its draft with $schema, and hand the schema and the data to a validator library. In JavaScript that usually means Ajv, in Python the jsonschema package, and in a terminal or CI job a CLI such as check-jsonschema. The validator answers yes or no and, when the answer is no, tells you which rule failed.

What you need

Three things. First, JSON that parses: a schema validator works on parsed data, so a stray trailing comma fails before any schema rule runs. Second, a schema. JSON Schema is "a vocabulary that you can use to annotate and validate JSON documents", and the schema is itself a JSON document. Third, a validator for your language, which the JSON Schema site describes as a tool that implements the specification: it takes a schema and an instance and returns the result.

Step 1: write the schema

Say an API returns users like this:

{ "id": 7, "email": "ana@example.com", "role": "admin" }

A schema for it:

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

What each piece does:

  • $schema says which draft of JSON Schema the schema follows. Validators use it to pick the rules.
  • $id gives the schema a URI, so other schemas can refer to it.
  • exclusiveMinimum: 0 means only values above zero pass. Use minimum if zero should be allowed.
  • required lists the keys that must exist. Without it, every property is optional: the object reference is explicit that properties are not required by default.
  • additionalProperties: false rejects keys the schema doesn't list. Without it, any additional property is allowed.

Step 2: validate in JavaScript with Ajv

Ajv compiles a schema into a validation function. It supports drafts 04, 06, 07, 2019-09 and 2020-12 (draft-04 needs the separate ajv-draft-04 package). The basic pattern from its README:

const Ajv = require("ajv")
const ajv = new Ajv({ allErrors: true })

const validate = ajv.compile(schema)
const valid = validate(data)
if (!valid) console.log(validate.errors)

Two details trip people up:

  1. The 2020-12 draft uses a different class. The default export does not handle it. The Ajv drafts page says draft-2020-12 is not backwards compatible, you can't mix it with older drafts in one instance, and you load it with const Ajv2020 = require("ajv/dist/2020").
  2. Ajv stops at the first error by default. The allErrors option makes it check all rules and collect every error. Turn it on when you show errors to people; leave it off on hot API paths if speed matters more.

Call compile once, at startup, and reuse the returned function for every document.

Step 3: validate in Python with jsonschema

The jsonschema package has a one-line form and a class form:

from jsonschema import validate, Draft202012Validator, ValidationError

try:
    validate(instance=data, schema=schema)
except ValidationError as e:
    print(e.message)

# or, to list every problem instead of stopping at the first
v = Draft202012Validator(schema)
for error in v.iter_errors(data):
    print(error.message)

validate() reads $schema to choose the draft. Without it, the library falls back to the latest released draft, and a schema written for an old draft can quietly behave differently under a newer one.

Step 4: validate from the command line or CI

For files in a repo (config files, fixtures, test data), a CLI is less work than a script. check-jsonschema is built on the Python library and works both as a command and as a pre-commit hook:

pipx install check-jsonschema
check-jsonschema --schemafile schema.json instance.json

The schema can be a local file or an HTTPS URL, which it downloads and caches. It also ships a check-github-workflows hook, so a typo in .github/workflows/*.yml fails on your machine instead of on push.

How to check it worked

Don't trust a schema until you've seen it reject something. Feed it a good document, which must pass. Delete a required key: that copy must fail on required. Then add an extra key such as "admin": true, which must fail on additionalProperties.

If that last one passes, additionalProperties: false is missing or sits on the wrong object. Nested objects need their own.

What goes wrong

Syntax errors look like schema errors. If the file doesn't parse, you never reach the schema. Errors like Unexpected end of JSON input or a rejected trailing comma come from the parser. Fix those first. If your file has comments, it's JSONC or JSON5 rather than JSON, and plain JSON parsers will reject it.

format doesn't validate anything by default. This is the big one. The JSON Schema type reference says that by default format is just an annotation and does not affect validation. So "format": "email" happily accepts "not-an-email" until you opt in. In Ajv, which includes no formats at all, add the ajv-formats plugin with addFormats(ajv). In Python, pass format_checker=Draft202012Validator.FORMAT_CHECKER. Even then, the Python checker returns true for any format it doesn't know, so a typo like "format": "emial" still passes silently.

Extra keys pass. A schema without additionalProperties: false accepts misspelled keys like "emial", so a config with a typo validates fine and then does nothing.

Draft mismatch. A schema copied from an old tutorial may use draft-07 keywords, while your validator assumes 2020-12, or the reverse. Declare $schema, and in Ajv pick the matching class.

My take

Validate at the edges and nowhere else: when data comes in from outside (an API request, a webhook, a config file someone edited by hand) and in CI for files in the repo. Past the boundary, types and tests do the job more cheaply. For outside input, always turn on format checking and additionalProperties: false. The defaults are lenient on purpose, for reuse and extension, but a lenient gate at the boundary is barely a gate.

A limitation to know

A schema checks shape, not meaning. It can say end_date is a date; it can't say end_date comes after start_date, or that a user ID exists in your database. Those checks still belong in code.

The same goes for the tools on this site. The OnlineViewer Dev JSON validator runs your input through JSON.parse in the browser and reports syntax errors, which is the first step above. It does not do JSON Schema validation, so for the schema step use one of the libraries or the CLI. If you're still choosing a format for a config file, JSON vs YAML covers that decision.

FAQ

Do I need to check that the JSON parses before running a schema validator?

Yes. A schema validator works on parsed data, so a missing bracket or trailing comma fails at the parsing step before any schema rule runs. Fix syntax errors first, then check the structure.

Why does my schema accept fields I never defined?

Because JSON Schema allows extra properties by default. Add "additionalProperties": false to the object schema if unknown keys should fail.

Why is an invalid email or date passing validation?

In current drafts, format is an annotation by default and does not affect validation. In Ajv add the ajv-formats plugin; in Python jsonschema pass a format checker such as Draft202012Validator.FORMAT_CHECKER.

Which JSON Schema draft should I use?

Use 2020-12 for new schemas unless a tool you depend on only supports an older draft. Declare it with $schema so validators pick the right rules, and in Ajv import the separate 2020 class.

Sources