Schema definition and validation
JSON Schema keywords from required to oneOf, how to avoid the composition traps, Avro schema basics, and wiring validation into CI so bad data never reaches production.
The keywords that carry the weight
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["id", "email"],
"additionalProperties": false,
"properties": {
"id": { "type": "integer", "minimum": 1 },
"email": { "type": "string", "format": "email" },
"role": { "type": "string", "enum": ["admin", "editor", "viewer"] },
"tags": { "type": "array", "items": { "type": "string" }, "maxItems": 10 },
"deletedAt": { "type": ["string", "null"], "format": "date-time" }
}
}requiredlists names that must be present; it does not check the value.additionalProperties: falseis what actually rejects typos in field names.enumandconstpin a value to a known set.formatis an annotation in the spec; most validators implementemail,date-timeanduri, but treat them as opt-in.
Composition and its traps
| Keyword | Means | Common mistake |
|---|---|---|
allOf | Must satisfy every subschema | Used for merging objects where additionalProperties then forbids siblings |
anyOf | At least one subschema matches | Left as the default when the type should be exclusive |
oneOf | Exactly one subschema matches | Overlapping branches make valid data fail |
$ref | Reference to another schema | Unresolvable remote refs breaking offline validation |
if/then/else | Conditional rules | Forgetting an else, so the negative case is unchecked |
{
"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 two branches must be mutually exclusive. Because card and bank narrow kind with const, no payload can satisfy both.
Validating in CI
import json
from jsonschema import Draft202012Validator
schema = json.load(open("schema/user.json"))
validator = Draft202012Validator(schema)
errors = sorted(validator.iter_errors(payload), key=lambda e: e.path)
for e in errors:
print("/".join(map(str, e.path)) or "<root>", "-", e.message)
assert not errors, f"{len(errors)} validation error(s)"Two CI habits pay off immediately: validate the schema itself against the meta-schema, and keep a folder of known-good and known-bad fixtures so a schema edit cannot silently loosen a rule.
💡
JSON Schema validates structure, not business meaning. If
endDate must follow startDate you need a custom check on top — the format was designed for shape, and layering semantic rules on keywords quickly becomes unreadable.FAQ
Do I need <code>additionalProperties: false</code>?
Almost always yes for internal contracts. Without it, a renamed field is accepted and silently ignored, and the mistake surfaces as missing data far from the cause.
JSON Schema or Avro schema?
JSON Schema for validating documents at the edge and for API contracts that humans read. Avro when the schema travels with the data in a binary pipeline and must support reader/writer evolution.
Related
Binary formats: Protocol Buffers, MessagePack, CBOR and BSON Validating and testing data pipelines
Last refreshed 2026-09-18.