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" }
  }
}
  • required lists names that must be present; it does not check the value.
  • additionalProperties: false is what actually rejects typos in field names.
  • enum and const pin a value to a known set.
  • format is an annotation in the spec; most validators implement email, date-time and uri, but treat them as opt-in.

Composition and its traps

KeywordMeansCommon mistake
allOfMust satisfy every subschemaUsed for merging objects where additionalProperties then forbids siblings
anyOfAt least one subschema matchesLeft as the default when the type should be exclusive
oneOfExactly one subschema matchesOverlapping branches make valid data fail
$refReference to another schemaUnresolvable remote refs breaking offline validation
if/then/elseConditional rulesForgetting 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.

Binary formats: Protocol Buffers, MessagePack, CBOR and BSON Validating and testing data pipelines

Last refreshed 2026-09-18.