JSON Schema Validation: Rules, Examples and API Testing
Quick answer
JSON Schema validation checks a JSON document against a schema that declares its allowed types, required fields, value ranges, formats and structure. A validator such as Python's jsonschema, JavaScript's Ajv or Java's networknt library returns every rule the data breaks. In API testing it verifies that each response body matches its documented contract.
Reviewed by Sushant Joshi
Part of the OpenAPI, Contract & Schema Testing hub. Start with the pillar guide: OpenAPI testing: how to test an API from its spec.
JSON Schema validation checks a JSON document against a schema: a separate JSON document that declares which types, fields, value ranges, string formats and structures are allowed. A validator compares the two and returns every rule the data breaks, each with the path to the failing value. In API testing, it is how you prove that a response body matches its documented contract, not just that one or two fields have the right values.
This guide is part of our OpenAPI testing hub. It covers the mechanics: the 2020-12 rules, working code for Python, JavaScript, Java and Postman, the OpenAPI 3.1 alignment and the common errors. To use schema validation in CI to catch an API drifting from its spec over time, read API schema validation: catching drift next.
What is JSON Schema validation, and how does it work?
A JSON Schema is itself JSON. Here is a small one:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["id", "email"],
"properties": {
"id": { "type": "integer", "minimum": 1 },
"email": { "type": "string", "format": "email" },
"role": { "enum": ["admin", "member", "viewer"] }
},
"additionalProperties": false
}
The validator walks the schema and the data together. Each keyword is an independent assertion about the value at the current location:
type: objectchecks the root value.requiredchecks which keys exist.propertiesapplies a sub-schema to each named key:idmust be an integer of at least 1, androlemust be one of three strings.additionalProperties: falserejects any key not listed.
The document is valid only if every applicable assertion passes. A good validator reports all failures, not just the first, so one test run shows you the whole problem.
Three properties make JSON Schema well suited to API testing:
- It is declarative, so the same schema works in every language.
- It checks structure as well as values.
body.id == 42says nothing about the other 30 fields; a schema checks all of them. - It is already in your spec. OpenAPI describes bodies with JSON Schema, so the schemas usually exist before you write a test.
JSON Schema drafts and validation rules
JSON Schema has evolved through numbered drafts: draft-04, 06 and 07, then 2019-09 and 2020-12, the current version. The $schema keyword says which one a schema uses, and the validator must support that draft. The 2020-12 changes you'll notice most:
prefixItemsreplaces the array form ofitemsfor tuples;itemsnow means "all remaining items".$defsreplacesdefinitionsas the standard place for reusable sub-schemas.unevaluatedPropertiesandunevaluatedItemsclose objects and arrays correctly even acrossallOfand$ref.additionalPropertiescannot do that.formatis an annotation by default. Validators only enforce it when you turn format assertion on. (More on this under common errors.)
The rules (keywords) you'll use most in API tests:
| Purpose | Keywords | Example |
|---|---|---|
| Type | type | "type": ["string", "null"] |
| Object structure | properties, required, additionalProperties, unevaluatedProperties, propertyNames, minProperties | "required": ["id", "name"] |
| Fixed values | enum, const | "enum": ["available", "pending", "sold"] |
| Strings | minLength, maxLength, pattern, format | "pattern": "^[A-Z]{3}$" |
| Numbers | minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf | "exclusiveMinimum": 0 |
| Arrays | items, prefixItems, minItems, maxItems, uniqueItems, contains | "minItems": 1 |
| Combining | allOf, anyOf, oneOf, not | "oneOf": [{...}, {...}] |
| Conditionals | if / then / else, dependentRequired | "if type is card, require last4" |
| Reuse | $ref, $defs, $id | "$ref": "#/$defs/Address" |
The rest of this guide uses one example schema: a user record from the public JSONPlaceholder API, GET /users/1. Save it as user.schema.json:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/user.schema.json",
"title": "User",
"type": "object",
"required": ["id", "name", "username", "email", "address"],
"properties": {
"id": { "type": "integer", "minimum": 1 },
"name": { "type": "string", "minLength": 1 },
"username": { "type": "string", "pattern": "^[A-Za-z0-9_.]+$" },
"email": { "type": "string", "format": "email" },
"phone": { "type": "string" },
"website": { "type": "string" },
"address": {
"type": "object",
"required": ["street", "city", "zipcode", "geo"],
"properties": {
"street": { "type": "string" },
"suite": { "type": "string" },
"city": { "type": "string" },
"zipcode": { "type": "string" },
"geo": {
"type": "object",
"required": ["lat", "lng"],
"properties": {
"lat": { "type": "string", "pattern": "^-?\\d+(\\.\\d+)?$" },
"lng": { "type": "string", "pattern": "^-?\\d+(\\.\\d+)?$" }
}
}
}
},
"company": {
"type": "object",
"required": ["name"],
"properties": { "name": { "type": "string" } }
}
},
"additionalProperties": false
}
Notice that lat and lng are strings that look like numbers. JSONPlaceholder returns them that way, and a schema should describe what the API actually promises. If they ever become real numbers, the test flags a contract change clients need to hear about.
JSON Schema validation in Python with jsonschema
jsonschema is the standard Python library and supports every draft up to 2020-12. Install it with pytest and requests:
python -m pip install jsonschema requests pytest
Save this as test_user_schema.py, next to the schema:
import json
from pathlib import Path
import requests
from jsonschema import Draft202012Validator, FormatChecker
SCHEMA = json.loads(Path("user.schema.json").read_text())
Draft202012Validator.check_schema(SCHEMA) # fail fast on a broken schema
VALIDATOR = Draft202012Validator(SCHEMA, format_checker=FormatChecker())
def test_user_matches_schema():
resp = requests.get("https://jsonplaceholder.typicode.com/users/1", timeout=10)
assert resp.status_code == 200
errors = list(VALIDATOR.iter_errors(resp.json()))
assert not errors, "\n".join(
f"{e.json_path}: {e.message}" for e in errors)
Run it with pytest -q; it passes. Three details matter:
check_schemavalidates the schema itself against the 2020-12 meta-schema, so a typo such as"type": "interger"fails at import instead of silently matching nothing.iter_errorscollects every violation.validate()raises only the first.format_checker=FormatChecker()turnsformatinto a real check. Without it,"email": "not-an-email"passes.
Score your OpenAPI spec (free)
Free, runs in your browser, no sign-up needed.
To see what failures look like, validate a deliberately broken payload:
bad = {"id": "1", "name": "", "username": "Bret", "email": "not-an-email",
"address": {"street": "x", "city": "y", "zipcode": "z",
"geo": {"lat": "-37.3", "lng": 81.1}},
"nickname": "B"}
for e in VALIDATOR.iter_errors(bad):
print(f"{e.json_path}: {e.message}")
That prints five errors, one per broken rule, including '1' is not of type 'integer', 'not-an-email' is not a 'email' and 'nickname' was unexpected. Each comes with a JSON path, so you know exactly where to look.
Your OpenAPI spec already holds these schemas. Paste it into the free OpenAPI test-readiness scorer to see which operations are missing request or response schemas. Those are the operations where schema validation has nothing to check against.
JSON Schema validation in JavaScript with Ajv
Ajv is the most widely used JavaScript validator. It compiles each schema into a fast validation function. For 2020-12, import the Ajv2020 class, and add ajv-formats for format checks:
npm install ajv ajv-formats
// validate-user.mjs (Node 18+ for built-in fetch)
import Ajv2020 from "ajv/dist/2020.js";
import addFormats from "ajv-formats";
import { readFile } from "node:fs/promises";
const schema = JSON.parse(await readFile("./user.schema.json", "utf8"));
const ajv = new Ajv2020({ allErrors: true, strict: true });
addFormats(ajv); // without this, compile() throws: unknown format "email"
const validate = ajv.compile(schema); // compile once, reuse many times
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
if (res.status !== 200) throw new Error(`Expected 200, got ${res.status}`);
const user = await res.json();
if (!validate(user)) {
for (const err of validate.errors) {
console.error(`${err.instancePath || "/"} ${err.message}`, err.params);
}
process.exit(1);
}
console.log("users/1 matches user.schema.json");
Run it with node validate-user.mjs. With the broken payload from the Python section, Ajv reports errors such as /id must be integer and /email must match format "email", plus must NOT have additional properties with {"additionalProperty":"nickname"}.
Ajv behaviours to know:
allErrors: truecollects every error. The default stops at the first.- Compile once, at module load, not inside each test.
ajv.compileis the expensive step. - Strict mode (on by default) refuses unknown keywords. That catches typos, but OpenAPI schemas often contain
exampleorxml, which fail withstrict mode: unknown keyword: "example". Register those keywords withajv.addKeyword("example")or setstrict: falsefor OpenAPI-sourced schemas. - The default
Ajvclass targets draft-07. UseAjv2020(orAjv2019) when your schemas declare a newer$schema.
JSON Schema validation in Java and Postman
Java with networknt json-schema-validator
The networknt json-schema-validator supports draft-04 through 2020-12 and works on Jackson JsonNodes. Here is the dependency (1.5.x line):
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
<version>1.5.6</version>
</dependency>
With user.schema.json on the classpath (for example in src/test/resources):
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.ValidationMessage;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Set;
public class UserSchemaCheck {
public static void main(String[] args) throws Exception {
JsonSchemaFactory factory =
JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012);
JsonSchema schema;
try (InputStream in = UserSchemaCheck.class.getResourceAsStream("/user.schema.json")) {
schema = factory.getSchema(in);
}
HttpResponse<String> res = HttpClient.newHttpClient().send(
HttpRequest.newBuilder(URI.create("https://jsonplaceholder.typicode.com/users/1")).build(),
HttpResponse.BodyHandlers.ofString());
if (res.statusCode() != 200) {
throw new AssertionError("Expected 200, got " + res.statusCode());
}
JsonNode body = new ObjectMapper().readTree(res.body());
Set<ValidationMessage> errors = schema.validate(body);
errors.forEach(e -> System.err.println(e.getMessage())); // e.g. "$.id: string found, integer expected"
if (!errors.isEmpty()) {
throw new AssertionError(errors.size() + " schema violation(s)");
}
System.out.println("users/1 matches user.schema.json");
}
}
In JUnit, move the validate call into a test and assert the set is empty. The library's 2.x line reorganises this API, so match the code to your version.
If you already use REST Assured, its json-schema-validator module gives a one-line matcher: .body(matchesJsonSchemaInClasspath("user.schema.json")). Be aware that this module is built on an older validator that targets draft-04. A schema that relies on 2020-12 keywords such as prefixItems, $defs or unevaluatedProperties needs a validator that supports that draft, such as networknt.
Postman
Postman test scripts include a jsonSchema matcher for responses:
const userSchema = {
type: "object",
required: ["id", "name", "username", "email", "address"],
properties: {
id: { type: "integer", minimum: 1 },
name: { type: "string", minLength: 1 },
email: { type: "string" },
address: {
type: "object",
required: ["street", "city", "zipcode", "geo"]
}
}
};
pm.test("GET /users/1 matches the user schema", () => {
pm.response.to.have.status(200);
pm.response.to.have.jsonSchema(userSchema);
});
Keep Postman schemas draft-07-compatible unless you have confirmed your Postman version handles newer keywords. Older collections that call tv4.validate should move to jsonSchema, because tv4 only supports draft-04.
Free PDF + code examples
OpenAPI to Test Generation Template Pack
Go from OpenAPI spec to full test coverage. Includes sample specs, example generated tests, edge case patterns, and CI/CD integration guides.
Download FreeJSON Schema and OpenAPI 3.1
OpenAPI describes request and response bodies with a Schema Object. How closely that object matches JSON Schema depends on the OpenAPI version:
| OpenAPI 3.0 | OpenAPI 3.1 | |
|---|---|---|
| Schema language | An extended subset of an early JSON Schema draft | Full JSON Schema 2020-12, plus a few OpenAPI keywords (discriminator, xml, externalDocs, example) |
| Null values | nullable: true | type: ["string", "null"] |
| Type arrays | Not allowed | Allowed |
exclusiveMinimum / exclusiveMaximum | Boolean modifiers on minimum / maximum | Numbers, as in JSON Schema |
| Examples | example (single) | examples (array) preferred; example deprecated |
$schema, $defs, if/then/else, const | Not supported | Supported |
This has two practical consequences for testing:
- With OpenAPI 3.1, response schemas are JSON Schema 2020-12. You can pull a schema out of
components.schemas, keepcomponentsalongside it so$refs resolve, and pass it straight toDraft202012ValidatororAjv2020. Our OpenAPI testing guide does exactly that against the Swagger Petstore. - With OpenAPI 3.0, translate first or use an OpenAPI-aware validator.
nullableand the booleanexclusiveMinimummean different things to a plain JSON Schema validator. Ajv supportsnullablenatively. In Python, an OpenAPI-aware library such as openapi-core handles the 3.0 dialect for you.
Spec-driven tools build on the same idea. Shift-Left API, for example, imports OpenAPI 3.x and Swagger 2.0 specs, and its generated tests include field and type checks derived from each operation's response schema. See OpenAPI test automation and how tests are generated from an OpenAPI spec. Whichever route you take, schema validation is one layer of API contract testing. It confirms the shape of a response; it can't confirm that the values are right for the request you sent.
Common JSON Schema validation errors and how to fix them
"'x' is a required property" / "must have required property 'x'". The response omitted a field the schema requires. Either the API has a bug, or the field is genuinely optional in some states (a draft order with no shippedAt). In the second case, remove it from required, or use if/then to require it only in the states where it must exist.
"'1' is not of type 'integer'". IDs, money and large numbers often arrive as strings. Decide what the contract is and make the schema say so. Don't loosen everything to "type": ["string", "integer"] to make a test pass.
"None is not of type 'string'" (null not allowed). The API returned null. In JSON Schema and OpenAPI 3.1, allow it with "type": ["string", "null"]. In OpenAPI 3.0, use nullable: true. Only allow null where clients really handle it.
"Additional properties are not allowed". The API added a field. With additionalProperties: false, that fails, which is useful for catching unplanned changes but noisy if additive changes are your normal practice. Choose deliberately. Also note that additionalProperties doesn't see properties declared inside allOf branches; use unevaluatedProperties: false for composed schemas.
Format checks silently pass. In 2019-09 and 2020-12, format is annotation-only unless you enable assertion. In Python, pass FormatChecker(), and note that some formats need extra packages: date-time is only checked if rfc3339-validator is installed. In Ajv, add ajv-formats. An unchecked date-time field will happily accept "yesterday".
"strict mode: unknown keyword" (Ajv). The schema contains a keyword Ajv doesn't know, often an OpenAPI example or xml, or a typo. Register the keyword or relax strict mode for spec-sourced schemas. Fix typos rather than suppressing them.
"can't resolve reference" / unresolvable $ref. The validator can't find the referenced schema. For a schema extracted from an OpenAPI document, keep components in the same object so #/components/schemas/... resolves. For multi-file schemas, give each an $id and register them with the validator.
Duplicate keys go unnoticed. JSON Schema validates parsed data, and most parsers keep only the last value of a duplicated key. If duplicates matter, detect them in the parser (for example with Python's object_pairs_hook).
Validation works in CI but not in VS Code (or vice versa). Editors use the schema mapped in their settings (json.schemas in VS Code) and may support a different draft from your test library. Treat the test suite as the source of truth. For YAML, parse it to data first, then validate exactly as you would JSON.
JSON Schema validation FAQ
What is JSON Schema validation?
JSON Schema validation is checking a JSON document against a JSON Schema, a JSON document that declares which types, properties, required fields, value ranges and formats are allowed. A validator reports every rule the data breaks, with the path to the failing value, or confirms that the data is valid.
How does JSON Schema validation work?
The validator walks the schema and the data together. Each keyword, such as type, required, properties, minimum or pattern, is an independent assertion applied to the value at the current location. Sub-schemas apply to nested objects and array items. The result is valid only if every applicable assertion passes.
What are the most important JSON Schema validation rules?
type, required, properties and additionalProperties define structure. enum and const restrict values. minLength, maxLength and pattern constrain strings, while minimum, maximum and multipleOf constrain numbers. items, minItems and uniqueItems constrain arrays. allOf, anyOf and oneOf combine schemas, and $ref and $defs let you reuse them.
Why does my JSON Schema ignore the format keyword?
In draft 2019-09 and 2020-12, format is an annotation by default, not an assertion. Enable format checking explicitly. In Python's jsonschema, pass a FormatChecker, and install optional packages such as rfc3339-validator for date-time. In Ajv, add the ajv-formats plugin. Without that, an invalid email or date can pass validation.
How do I allow null or a string in JSON Schema?
Use a type array: "type": ["string", "null"]. That works in JSON Schema and in OpenAPI 3.1. OpenAPI 3.0 does not allow type arrays and uses "nullable": true alongside a single type instead.
How is JSON Schema validation used in API testing?
Each test sends a request, parses the JSON response and validates it against the schema documented for that endpoint and status code, usually taken from the OpenAPI spec. It catches missing fields, wrong types, unexpected nulls and new undocumented fields that value-level assertions miss.
Sources and further reading
- JSON Schema specification, JSON Schema organisation
- JSON Schema 2020-12: Core and Validation vocabularies
- Understanding JSON Schema, the official guide to keywords
- python-jsonschema documentation, including format validation
- Ajv documentation and ajv-formats
- networknt json-schema-validator
- REST Assured JSON Schema validation
- OpenAPI Specification v3.1.0: Schema Object
- OpenAPI Specification v3.0.3: Schema Object
- JSONPlaceholder, the free fake REST API used in the examples
Ready to shift left with your API testing?
Import your OpenAPI spec and get a runnable test suite, in our cloud or on your own infrastructure.