How to validate JSON against schema

Run ajv validate --spec=draft2020 -c ajv-formats -s user.schema.json -d user.json. A document that conforms prints user.json valid and exits 0. A document that breaks the schema prints one entry per violation, each carrying an instancePath that points at the offending field, and exits 1.

Why check this

A field that changes type from integer to string breaks every typed client that reads it, and no status code reports that. Schema validation turns the contract into a test you can run in regression, on every build, and again after a database migration renames a column. Run it on the payload you captured, before anyone argues about whose side is wrong.

Prerequisites

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["id", "email", "createdAt"],
  "additionalProperties": false,
  "properties": {
    "id": { "type": "integer", "minimum": 1 },
    "email": { "type": "string", "format": "email" },
    "createdAt": { "type": "string", "format": "date-time" },
    "roles": {
      "type": "array",
      "minItems": 1,
      "items": { "enum": ["admin", "editor", "viewer"] }
    }
  }
}

A payload that conforms, user.json:

{
  "id": 42,
  "email": "tester@example.com",
  "createdAt": "2026-09-11T08:30:00Z",
  "roles": ["editor"]
}

And the payload under test, user-bad.json:

{
  "id": "42",
  "email": "tester@example.com",
  "roles": ["owner"],
  "nickname": "t"
}

Steps

  1. Step 1.

    Compile the schema on its own, before any data touches it.

    npx --yes -p ajv-cli@5.0.0 -p ajv-formats@3.0.1 ajv compile --spec=draft2020 -c ajv-formats -s user.schema.json
    
    schema user.schema.json is valid

    A schema that fails here makes every later result meaningless, so this line comes first.

  2. Step 2.

    Validate the payload that is supposed to conform.

    npx --yes -p ajv-cli@5.0.0 -p ajv-formats@3.0.1 ajv validate --spec=draft2020 -c ajv-formats -s user.schema.json -d user.json
    
    user.json valid
  3. Step 3.

    Validate the payload under test with --all-errors, so the run reports every violation instead of stopping at the first.

    npx --yes -p ajv-cli@5.0.0 -p ajv-formats@3.0.1 ajv validate --spec=draft2020 -c ajv-formats --all-errors -s user.schema.json -d user-bad.json
    
    user-bad.json invalid
    [
    {
      instancePath: '',
      schemaPath: '#/required',
      keyword: 'required',
      params: { missingProperty: 'createdAt' },
      message: "must have required property 'createdAt'"
    },
    {
      instancePath: '',
      schemaPath: '#/additionalProperties',
      keyword: 'additionalProperties',
      params: { additionalProperty: 'nickname' },
      message: 'must NOT have additional properties'
    },
    {
      instancePath: '/id',
      schemaPath: '#/properties/id/type',
      keyword: 'type',
      params: { type: 'integer' },
      message: 'must be integer'
    },
    {
      instancePath: '/roles/0',
      schemaPath: '#/properties/roles/items/enum',
      keyword: 'enum',
      params: { allowedValues: [Array] },
      message: 'must be equal to one of the allowed values'
    }
    ]

    Read instancePath as a pointer into the data and schemaPath as a pointer into the schema. The first two entries carry an empty instancePath because the fault is the object itself, not one of its fields.

  4. Step 4.

    Read the exit code, which is what a pipeline reacts to.

    echo $?
    
    1

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | user.json valid, exit 0 | Every keyword in the schema held | Nothing. Keep the command in the pipeline. | | keyword: 'type' at /id | The field is there with the wrong JSON type | Decide which side is wrong. A quoted number is usually a serialiser default. | | keyword: 'required' with an empty instancePath | A mandatory field is absent from the payload | Treat it as a contract break, not as a null value. | | keyword: 'additionalProperties' | The payload carries a field the schema does not list | Either the API grew a field or the schema went stale. | | schema ... is invalid | The schema does not compile | Fix the schema. Any data result is unusable until it does. |

Common mistakes

Sign: The run stops with: unknown format email ignored in schema at path #/properties/email.Cause: ajv ships without the format vocabulary. Calling ajv-cli on its own rejects the schema, so both packages have to reach the same npx call and -c ajv-formats has to be passed. Deleting format from the schema hides the constraint instead of checking it.
Sign: A payload with an unexpected extra field passes.Cause: JSON Schema allows unknown properties unless additionalProperties is false. A schema written from a sample response accepts anything the API later adds, which is the drift this check exists to catch.
Sign: Only the first error is reported and the run ends.Cause: ajv short-circuits by default. Without --all-errors a payload with four faults looks like a payload with one, and the next round trip finds the next fault.

What to check next

FAQ

How to validate a JSON schema itself?

Use ajv compile with no -d flag, as in step 1. It reports unknown keywords, unresolved $ref targets and unknown formats. A schema that compiles is well formed. Whether it describes the right contract is a review question, not a tool question.

How to validate a JSON response from an API?

Save the body to a file first, then point -d at it. Validating a piped stream hides which bytes failed. Capturing the body also gives you an artefact to attach to the bug report when the schema rejects it.

How to validate a JSON schema in Postman?

Postman runs ajv inside a test script: pm.response.to.have.jsonSchema(schema). It defaults to draft-07, so a $schema line naming 2020-12 is ignored and keywords such as prefixItems stop working. Keep one schema file and run the CLI in CI.

How to validate JSON against an OpenAPI schema?

Copy the schema object out of components/schemas and feed it to ajv. OpenAPI 3.1 schemas are JSON Schema 2020-12, so they transfer without edits. OpenAPI 3.0 schemas use a dialect with nullable, which ajv rejects until that keyword is removed.

Verified

Verified by Maks Vernyajv-cli 5.0.0ajv-formats 3.0.1node 22.23.2

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

intermediate6 minpublished updated Maks Verny