JSON Schema is a vocabulary for describing and evaluating JSON instances. It can assert types, required fields, ranges, patterns, and relationships; it can also provide annotations for documentation and tooling. It is not an authentication system, an object-ownership check, a sanitizer, or a guarantee that a value is safe for a downstream interpreter.
Key Takeaways
- Declare the dialect with
$schemaand use validators configured for that dialect. Draft-07, 2019-09, and 2020-12 are not interchangeable feature sets. - Separate assertions that accept or reject an instance from annotations such as
title,description,default,readOnly, andexamples. formatbehavior depends on the dialect and validator configuration; enable the formats and strictness your contract actually requires.- Validate shape before business logic, but perform authentication, authorization, semantic checks, normalization, and side-effect policy separately.
- Use
$defs,$ref,allOf,anyOf,oneOf,if/then/else, andunevaluatedPropertiesdeliberately. Composition can create ambiguous or unexpectedly closed schemas. - Bound input size, depth, property counts, array lengths, regex complexity, reference expansion, and error output. Validation itself is an attack surface.
Dialects, Vocabularies, and $schema
The $schema URI identifies the dialect and meta-schema used to interpret keywords. A 2020-12 schema can use vocabularies and keywords that a draft-07 validator does not understand. Pin the validator version and test the exact schema in CI.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/user-create",
"type": "object",
"required": ["name", "email"],
"properties": {
"name": { "type": "string", "minLength": 1, "maxLength": 100 },
"email": { "type": "string", "format": "email" }
},
"additionalProperties": false
}
$id establishes an identity used for references and registries; it is not automatically a fetch permission or a network location the validator should retrieve. Disable arbitrary remote reference loading unless it is explicitly allowlisted and pinned.
Assertions and Annotations
Assertions participate in validation:
type,enum,const;required,properties,patternProperties,additionalProperties;items,prefixItems,contains,minItems,maxItems;minimum,maximum,multipleOf;allOf,anyOf,oneOf,not, and conditional keywords.
Annotations such as title, description, default, examples, deprecated, readOnly, and writeOnly describe intent or guide tooling. A default value does not cause a validator to insert that value unless an application explicitly implements defaulting. readOnly and writeOnly do not enforce an access-control policy by themselves.
Objects, Unknown Properties, and Composition
additionalProperties: false closes properties that are not declared in the same schema object. With allOf, properties declared in a sibling subschema may still be considered additional unless the composition is designed for it. In 2020-12, unevaluatedProperties can express a different boundary, but it also requires understanding annotation collection across composed schemas.
Use explicit property policies for public APIs:
- reject unknown fields when typos or mass assignment are dangerous;
- permit extension fields under a named namespace when forward compatibility matters;
- strip unknown fields only as a documented transformation, never as a hidden security decision;
- test composition with valid, invalid, and extension instances.
Strings, Numbers, and Formats
minLength and maxLength count Unicode code points according to the dialect, not necessarily user-perceived grapheme clusters or bytes. pattern is a regular expression whose supported syntax and performance depend on the validator; avoid catastrophic patterns and bound input length.
JSON numbers have no fixed precision in the data model, but a runtime may parse them into a limited numeric type. If identifiers or monetary values require exact decimal or integer behavior, use a string with a precise pattern/format or a number-preserving parser plus a domain rule.
format is not universally an assertion. A validator may treat it as annotation unless a format vocabulary or option enables assertion, and built-in checks may be intentionally permissive. Add a domain-specific semantic check for requirements such as deliverable email, allowed hostname, reachable URL, or business timezone.
Arrays and Conditional Schemas
Use prefixItems for tuple-like arrays and items for the remaining elements in 2020-12. uniqueItems compares JSON values; it does not mean objects have unique business IDs. For that requirement, validate an explicit identity rule in application code or a dedicated vocabulary.
Conditional schemas need a condition that cannot vacuously pass:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["country"],
"properties": {
"country": { "type": "string" },
"postalCode": { "type": "string" }
},
"if": {
"properties": { "country": { "const": "US" } },
"required": ["country"]
},
"then": {
"required": ["postalCode"],
"properties": { "postalCode": { "pattern": "^[0-9]{5}(-[0-9]{4})?$" } }
}
}
Without required inside if, an absent country can make the properties condition pass because there is nothing to contradict it.
References and Reusable Definitions
Use $defs for local reusable fragments and $ref for stable shared contracts:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"nonEmptyName": { "type": "string", "minLength": 1, "maxLength": 100 }
},
"type": "object",
"properties": {
"name": { "$ref": "#/$defs/nonEmptyName" }
},
"required": ["name"]
}
Reference resolution must be bounded and deterministic. Do not let untrusted schema input cause arbitrary network requests, uncontrolled recursion, or unbounded expansion.
JavaScript Validation with Ajv
Pin Ajv and its format package in the project, choose the dialect explicitly, and avoid mutating input during validation unless that behavior is a deliberate boundary:
import Ajv from "ajv";
import addFormats from "ajv-formats";
const schema = {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: {
id: { type: "string", minLength: 1, maxLength: 64 },
email: { type: "string", format: "email" }
},
required: ["id", "email"],
additionalProperties: false
};
const ajv = new Ajv({
strict: true,
allErrors: false,
removeAdditional: false,
});
addFormats(ajv);
const validate = ajv.compile(schema);
export function validateUser(input) {
const valid = validate(input);
return {
valid,
errors: valid ? [] : (validate.errors ?? []).map((error) => ({
instancePath: error.instancePath,
keyword: error.keyword,
params: error.params,
})),
};
}
Do not return the entire request, password, token, or secret-containing value in an error response. Validation success only establishes that the instance matched the configured assertions.
Python Validation
Use a validator class matching the declared dialect and pass a format checker only when its semantics are accepted by the application:
from jsonschema import Draft202012Validator, FormatChecker
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {"type": "string", "minLength": 1, "maxLength": 64},
"email": {"type": "string", "format": "email"},
},
"required": ["id", "email"],
"additionalProperties": False,
}
validator = Draft202012Validator(schema, format_checker=FormatChecker())
errors = sorted(validator.iter_errors({"id": "u-1", "email": "bad"}), key=str)
for error in errors:
print(error.json_path, error.validator, error.validator_value)
The library and its transitive dependencies must be pinned and tested. A format checker is not proof that an address is deliverable or that a URL is safe to request.
Validation Is Not Authorization
A schema can require an objectId string, but it cannot prove that the authenticated principal may read or modify that object. It can constrain a tool argument’s shape, but it cannot decide tenant ownership, network egress, file permissions, rate limits, or whether a side effect requires approval.
Apply the layers separately:
- authenticate the caller and establish trusted tenant/principal context;
- validate the untrusted instance against the correct schema;
- authorize the requested object, fields, operation, and side effect;
- normalize or transform only under an explicit policy;
- execute with budgets, idempotency, audit records, and output validation.
Schema Evolution and Compatibility
Changing required, narrowing an enum, reducing a maximum, closing additional properties, or changing a type can break existing producers or consumers. Adding an optional property is often easier to roll out, but additionalProperties: false and generated clients can still make it breaking.
Version schemas with an explicit compatibility policy:
- backward compatibility: old instances remain accepted;
- forward compatibility: new instances can be handled by old consumers;
- read/write compatibility: request and response rules may differ;
- migration: whether defaults, coercion, or transformations are applied outside validation.
Run old and new fixtures through both validators, publish deprecation windows, and do not treat a schema diff as a complete API compatibility proof.
Error Handling, Limits, and Observability
Return stable machine-readable error codes and bounded paths, not raw payloads or stack traces. Decide whether to fail on the first error or collect all errors; allErrors can increase CPU and expose more data. Limit schema size, reference depth, instance depth, array length, property count, string length, regex cost, and error output.
Record schema ID/version, validator version, dialect, policy options, result, and correlation ID. Do not log secrets or full invalid instances by default.
Frequently Asked Questions
Does JSON Schema sanitize input?
No. It validates selected assertions. It does not remove HTML, prevent SQL injection, authorize objects, verify ownership, or make a URL safe to fetch. Apply context-specific controls after validation.
Does default fill in missing values?
Not by the JSON Schema specification alone. default is an annotation. A framework may implement default insertion, but that mutation must be explicit, tested, and separated from pure validation.
Does format: email prove an email address is usable?
No. It may only perform a syntax check, and behavior depends on the validator and enabled format vocabulary. Deliverability, domain policy, verification, and abuse controls are separate.
Why can if/then validate unexpected instances?
An if schema can succeed when its distinguishing property is absent unless the condition requires that property. Add required inside the condition and test absent, matching, and non-matching cases.
Is additionalProperties: false always the safest setting?
No. It can catch typos, but it can also break forward compatibility and interact unexpectedly with allOf. Choose an extension and evolution policy, then test composed schemas.
Primary Sources
- JSON Schema 2020-12 Core
- JSON Schema 2020-12 Validation
- JSON Schema Understanding JSON Schema
- Ajv documentation
- python-jsonschema documentation
- OWASP: Input Validation Cheat Sheet
Conclusion
JSON Schema is a precise contract tool when its dialect, vocabularies, assertions, annotations, references, and limits are explicit. Use it to reject malformed or out-of-contract instances, then apply independent authorization, normalization, security, and business rules. Version the schema with real compatibility tests so validation improves reliability without becoming a false security boundary.