JsonMind
API & Contracts10 min readUpdated Nov 10, 2026

JSON Schema Validation: Common Errors and How to Fix Them

Diagnose JSON Schema validation failures, understand required, type, additionalProperties and format errors, and validate payloads before they reach production.

JMBy JsonMind EngineeringLast reviewed: Nov 10, 2026Reviewed against primary specifications and linked sources.
Quick answer

A JSON Schema validation error names the instance path and the keyword that failed. Read the path first, then the keyword: required means a property is missing, type means the value has the wrong JSON type, additionalProperties means an unexpected key is present, and format is usually annotation unless the validator explicitly enforces it.

Key takeaways
  • Read the instance path before the error message; it identifies the exact value that failed.
  • required, type, enum, pattern and additionalProperties cover most validation failures.
  • format is annotation by default; do not assume a validator rejects invalid emails or dates.
  • JSON Schema drafts differ; confirm the draft before relying on keywords such as unevaluatedProperties.
  • Validate at the boundary and keep runtime validation even when TypeScript types exist.

JSON Schema validation errors are usually precise. The message may be terse, but the instance path and the failing keyword tell you exactly what to inspect. The difficulty is knowing which keyword produced the error and whether the schema or the data is wrong.

This guide walks through the errors developers see most often and the fastest way to resolve them.

Read the instance path first

A validator usually returns a path such as:

#/items/0/price: must be number

The path points into the instance, not the schema. It means the price property of the first item in items failed the type keyword.

Start at that path. Then read the keyword:

KeywordWhat it checks
requiredA named property is missing from an object
typeThe JSON type is wrong
enumThe value is not one of the allowed values
patternA string does not match a regular expression
additionalPropertiesAn object contains a key the schema does not allow
minItems / maxItemsAn array has too few or too many items
oneOf / anyOf / allOfA composed schema branch failed

The message alone can be misleading when composition is involved. The path is the anchor.

The path is a URI fragment JSON Pointer. #/customer/address/zip walks three keys, and #/items/0 selects the first array element. Two escapes matter: ~1 means a literal / inside a key and ~0 means a literal ~, so a property named a/b appears as #/a~1b. A bare # is the whole document, which is what you see when the root itself fails a type check.

Validators expose the same information in different shapes. Ajv returns instancePath: "/items/0/price" without the leading #, plus keyword: "type", params: { type: "number" } and a schemaPath into the schema. Python's jsonschema raises ValidationError with absolute_path as a deque and a message such as '42' is not of type 'number'. Log the machine-readable fields, not only the message; messages change between validator versions and are not stable enough to assert on in tests.

When a response contains twenty errors, fix the first error at the shallowest path and re-run. Later errors are often consequences of the first one, especially when allErrors is enabled.

required: missing at the wrong level

A common mistake is putting required at the root when the property lives in a nested object.

{
  "type": "object",
  "properties": {
    "customer": {
      "type": "object",
      "properties": {
        "email": { "type": "string" }
      }
    }
  },
  "required": ["email"]
}

This schema requires an email property on the root object, not inside customer. The correct placement is inside the customer schema:

{
  "type": "object",
  "properties": {
    "customer": {
      "type": "object",
      "properties": {
        "email": { "type": "string" }
      },
      "required": ["email"]
    }
  },
  "required": ["customer"]
}

When a required error appears, check which object the path points to. Then confirm the required array belongs to that object's schema.

The failure usually looks like {instancePath: "/customer", keyword: "required", params: {missingProperty: "email"}} in Ajv, or 'email' is a required property in Python. Note what required does not check: an empty string, null, 0 and false all count as present. If those values are unacceptable, add minLength, type or a not constraint.

required is ignored when the instance is not an object, and it is meaningless on arrays. It also does not inherit: a child schema does not inherit the parent's required list. If a field should exist but may be null, omit it from required and use "type": ["string", "null"]; otherwise every client must send a placeholder value merely to satisfy the validator.

type: the value parsed, but the type is wrong

JSON has six value types: object, array, string, number, boolean and null. A validator will reject "42" when the schema says number, and reject 42 when the schema says string.

Do not "fix" this by switching the schema to ["string", "number"] unless the API truly accepts both. Decide the contract:

  • IDs should usually be strings when they are not used for arithmetic.
  • Counts and amounts should be numbers, with a separate decision about precision.
  • Booleans should not be represented as "true" / "false" strings without a documented reason.

A coercing validator may accept the wrong type and convert it. That hides contract drift. Prefer strict validation at the API boundary.

Two details cause surprising failures. integer is a separate keyword from number: 1.0 validates as an integer because its mathematical value has no fractional part, while 1.5 fails with must be integer. And JSON numbers are not arbitrary precision: JavaScript parses them into IEEE 754 doubles, so an integer above 9,007,199,254,740,991 has already lost precision before any validator sees it. Store such identifiers, and monetary amounts in minor units or strings, rather than relying on 64-bit integers in JSON.

If a validator runs with coerceTypes, "42" becomes 42 in place, and the value you validate is no longer the value you received. Keep coercion off at trust boundaries. Reject NaN and Infinity explicitly when validating in-memory JavaScript objects; neither is valid JSON, but a validator can still encounter them before serialization.

additionalProperties: unexpected keys

The default for additionalProperties is true in JSON Schema. If a schema does not define a property and does not restrict additional keys, the validator accepts them.

{
  "type": "object",
  "properties": {
    "id": { "type": "string" }
  },
  "additionalProperties": false
}

With additionalProperties: false, any key other than id fails. This is useful for catching typos and accidental data leakage. It is also a common source of errors when an API adds a field before the schema is updated.

Use additionalProperties: false deliberately. If the object is intentionally open, document that and consider propertyNames or a pattern-based schema instead.

The typical errors are must NOT have additional properties with params.additionalProperty: "foo" in Ajv and Additional properties are not allowed ('foo' was unexpected) in Python. Properties matched by patternProperties count as evaluated and are allowed. A subtler trap appears with allOf: if one branch sets additionalProperties: false but declares only its own properties, it rejects fields declared in a sibling branch. Put the closure at the outer level with unevaluatedProperties: false in draft 2019-09 or later, or repeat every allowed property in each branch.

Closing an object is a compatibility decision. Rejecting unknown keys catches typos and client drift, but it also breaks old clients when a new optional field is added to a request. Many teams log unknown keys and return a warning header during a deprecation window, then switch to rejection. That preserves forward compatibility without silently accepting garbage.

format is annotation by default

A schema may say:

{ "type": "string", "format": "email" }

Many validators treat format as an annotation and do not reject an invalid email. Some drafts and validators allow format assertion; others require an opt-in option.

If email or date validation matters, combine format with a pattern, or validate in application code. Do not assume the schema rejected a malformed value unless the validator is configured to assert formats.

Draft 2019-09 formalized this by splitting format into an annotation vocabulary and an assertion vocabulary; whether a validator asserts depends on the vocabulary it enables. Ajv needs the separate ajv-formats package, and even then its email check is a pragmatic shape test, not RFC 5322. date-time expects an RFC 3339 timestamp with an offset, so 2026-11-10 10:00:00 fails while 2026-11-10T10:00:00Z passes.

A related surprise is pattern: it is an unanchored ECMA-262 regex, so "pattern": "[0-9]{4}" accepts "abc1234xyz". Anchor it as ^[0-9]{4}$ when the whole string must match. For phone numbers, national IDs and postal codes, no standard format keyword fits; validate with your own library and keep format as documentation.

oneOf, anyOf and allOf

Composition is powerful and easy to misread:

  • anyOf: at least one branch must pass.
  • oneOf: exactly one branch must pass.
  • allOf: every branch must pass.

oneOf is the most common source of confusing errors. If two branches match, the instance fails even though each branch individually passed. Error messages may list failures from every branch, which makes it look like nothing matched.

A useful pattern is to discriminate on a constant field:

{
  "oneOf": [
    {
      "properties": {
        "kind": { "const": "card" },
        "last4": { "type": "string", "pattern": "^[0-9]{4}$" }
      },
      "required": ["kind", "last4"]
    },
    {
      "properties": {
        "kind": { "const": "bank" },
        "iban": { "type": "string" }
      },
      "required": ["kind", "iban"]
    }
  ]
}

The const value makes the branches mutually exclusive, so oneOf behaves predictably.

The error shape is worth recognizing. Ajv reports must match exactly one schema in oneOf and sets params.passingSchemas: an array when several branches matched, null when none did. Python reports is valid under each of ... when several match. The error does not tell you which branch was intended, so compare the instance against each branch yourself.

The most common mistake is omitting required inside each branch. properties only validates keys that are present, so {} satisfies every branch without a required or additionalProperties constraint, and oneOf fails on an empty object. Add required to every branch, and close each branch with additionalProperties: false when branches must be mutually exclusive. If exclusivity is not actually required, anyOf is cheaper and produces clearer errors; oneOf must evaluate every branch.

Draft differences and $ref

JSON Schema has multiple drafts. Keywords such as unevaluatedProperties, prefixItems and $dynamicRef are not available in older drafts. A validator may also implement a subset of a draft.

Before debugging a keyword that "does nothing", confirm:

  1. The schema declares the draft with $schema.
  2. The validator supports that draft.
  3. The keyword is not being ignored as an unknown annotation.

$ref resolution is another common failure point. A missing or incorrect $id can make a reference resolve to the wrong document. Keep schemas close to the code that uses them, and validate that every $ref resolves before deploying.

Pin the draft explicitly. Draft 7 uses http://json-schema.org/draft-07/schema# and definitions; 2020-12 uses https://json-schema.org/draft/2020-12/schema and $defs. In 2020-12, the array form of items moved to prefixItems, and dependencies split into dependentRequired and dependentSchemas. Keywords next to $ref are ignored in draft 7 but applied in 2019-09 and later. $id changes the base URI for relative references, so two copies of the same fragment can resolve differently depending on where they are embedded.

Bundle referenced schemas into $defs at build time instead of fetching them at request time; a network fetch inside a validator turns latency, DNS and availability into validation failures. Validate the schema itself against its metaschema in CI. Most reports that "the keyword does nothing" end with a typo such as aditionalProperties, which is a valid unknown annotation and is therefore ignored without an error.

Validate at the boundary

A schema is a runtime contract. TypeScript types are erased, so they cannot protect a network boundary on their own.

const schema = await loadSchema("order.schema.json");
const result = validator.validate(schema, payload);

if (!result.valid) {
  throw new ValidationError(result.errors);
}

Validate:

  • Request bodies before business logic.
  • Third-party webhook payloads before trusting fields.
  • Configuration files before starting a service.
  • Stored JSON before migrating it.

The JSON Schema validator can check a payload against a schema in the browser. The JSON to JSON Schema generator creates a first draft from a sample, and the OpenAPI viewer helps inspect schema components in an API contract. Use the JSON formatter to inspect the raw payload before validating.

Compile the schema once at process start, not on every request. Ajv caches compiled validators by schema object, and compilation is the expensive part; re-reading and recompiling per request can dominate latency. Use allErrors: true for API responses so clients can fix several fields at once, but fail fast for configuration where the first error is enough. Return instancePath, keyword and params in your error payload; do not return schemaPath or the whole schema, which leaks internal structure.

Validation does not replace resource limits. A 100 MB body blocks the event loop during JSON.parse long before any schema runs, so enforce a byte limit at the HTTP layer. Parse into null-prototype objects, or reject __proto__ and constructor keys, if the payload is later merged into application objects; JSON Schema treats __proto__ as an ordinary property and will not protect you from prototype pollution.

JSON Schema is the wrong tool when the contract is mostly behavioral (rate limits, ordering, cross-field arithmetic), when payloads are multi-megabyte blobs, or when one team controls both ends and a generated validator adds more maintenance than it removes. In those cases use a typed decoder or a hand-written boundary check, and keep JSON Schema for the public API description.

A debugging checklist

  1. Read the instance path.
  2. Identify the failing keyword.
  3. Check the correct nesting level.
  4. Confirm the JSON type.
  5. Check additionalProperties and unexpected keys.
  6. Check composition branches.
  7. Confirm the draft and validator support.
  8. Confirm every $ref resolves.
  9. Re-run with a minimal payload.
  10. Add the failing case to a test.
  11. Remove one keyword or property at a time to find the smallest change that flips the result.

A validation error is not noise. It is the schema telling you where the contract and the data disagree.

Frequently asked questions

What does a JSON Schema error path like #/items/0/price mean?

It points to the exact location in the instance: the first item in the items array, and its price property. Fix the value at that path, not the whole document. The path is usually more useful than the human-readable message.

Why does my schema say a property is required when I included it?

Check the nesting level. required applies to the object that contains the property, not to the root. If the property is inside a nested object, the required array must be in that nested schema.

Does JSON Schema validate format automatically?

In many validators, format is an annotation by default. Some drafts and validators support format assertion or an opt-in format checker. Do not rely on format alone for email, date-time or URI validation.

What is the difference between oneOf, anyOf and allOf?

anyOf passes when at least one subschema passes. oneOf passes when exactly one subschema passes. allOf requires every subschema to pass. oneOf is useful for mutually exclusive shapes but produces confusing errors when several branches match.

Should I use JSON Schema or TypeScript types?

Use both. TypeScript types are erased at runtime, so they cannot validate network input. JSON Schema can validate at runtime and is also useful for documentation and code generation. Keep the schema as the source of truth when an API contract exists.

References

Put this guide into practice

Browse all tools

Open the browser tool. Inputs stay in your browser and are not uploaded to the business server.

Related guides

All guides