Schema Keywords That Silently Widen Validation
Your schema says it validates the payload. It does — just not the payload you think. A validator can return true on a document that's missing required business fields, carrying injected keys, or structurally wrong, because two keywords you wrote independently are quietly canceling each other out. The schema isn't broken; the interaction is. This is one of the most common sources of false-green test runs in data-heavy pipelines.
The problem sits at the intersection of JSON Schema's composability and its evaluation-order semantics. Keywords like additionalProperties, unevaluatedProperties, anyOf, if/then/else, and $ref each have well-defined individual behavior. Combined, they produce emergent validation boundaries that are wider than any single author intended — and wider than any single reviewer notices during PR review.
By the end of this article you'll be able to identify the four most dangerous keyword combinations, reproduce the silent-pass behavior in a test harness, and write schemas that fail loudly when the boundary shifts.
Learn Node.js, Cucumber, GitHub Copilot, APIs, CI/CD, and modern automation by building a complete framework.
How Keyword Composition Quietly Expands What a Schema Accepts
JSON Schema 2020-12 is an applicator-based grammar: keywords like allOf, anyOf, oneOf, and if/then/else each run their sub-schemas against the same instance node, and their results are AND-ed or OR-ed at the parent level. additionalProperties is evaluated only against properties not already named in properties or matched by patternProperties — but crucially, it has no visibility into what sibling applicators like anyOf branches have already "claimed." This is the root of most silent-widening bugs: you assume an applicator narrows; it actually creates an escape hatch.
unevaluatedProperties (introduced in draft 2019-09) was designed to close that gap. It sees annotations from all applicators in scope, not just the immediate sibling keywords. But mixing additionalProperties: false in one branch of an anyOf with unevaluatedProperties at the parent produces contradictory annotation propagation that most validators resolve permissively. Understanding where your schema sits in a test data validation pipeline determines whether that permissiveness ever surfaces as a failure.
Reproducing and Closing Silent-Pass Gaps in Four Patterns
The fastest way to audit an existing schema is to throw property-based inputs at it with Hypothesis + Schemathesis, then inspect which generated documents pass that shouldn't. But first, you need to be able to reproduce the four canonical patterns by hand.
Pattern 1 — additionalProperties + anyOf
{
"type": "object",
"anyOf": [
{ "properties": { "cat": { "type": "string" } }, "additionalProperties": false },
{ "properties": { "dog": { "type": "string" } }, "additionalProperties": false }
]
}
Pass {"cat": "tabby", "dog": "lab"} to any major validator (jsonschema 4.x, ajv 8.x). It validates. The additionalProperties: false in branch one doesn't see dog as additional because branch two already claimed it — but branch two's constraint never fires because the document satisfies branch one. Lift additionalProperties: false to the root and the document correctly fails.
Pattern 2 — $ref + sibling keywords (pre-2019-09 behavior)
# Draft-07 and earlier: sibling keywords next to $ref are IGNORED
{
"$ref": "#/$defs/BaseEvent",
"required": ["correlation_id"] # silently ignored in draft-07
}
In JSON Schema draft-07, any keyword adjacent to $ref is discarded by spec. If your validator defaults to draft-07 (Postman's built-in schema validator still does as of v10), that required extension is dead code. Explicitly declare "$schema": "https://json-schema.org/draft/2020-12/schema" at the root and switch to allOf: [{"$ref": "..."}, {"required": [...]}] for backward-compatible environments.
Pattern 3 — unevaluatedProperties with nested allOf
import jsonschema, json
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [{"properties": {"id": {"type": "integer"}}}],
"unevaluatedProperties": False
}
bad_doc = {"id": 1, "injected_field": "surprise"}
# Raises ValidationError — correct
jsonschema.validate(bad_doc, schema)
This works. Now wrap the allOf inside an anyOf branch and unevaluatedProperties at the parent no longer sees the annotations from the inner allOf in all validators — ajv 8 handles it correctly, but jsonschema 4.21 has a known annotation-propagation gap (issue #1283). Pin your validator version and add a canary test that asserts the bad document above fails; treat a passing canary as a CI break.
Pattern 4 — if/then without else
{
"if": { "properties": { "role": { "const": "admin" } }, "required": ["role"] },
"then": { "required": ["permissions"] }
}
When role is absent, the if sub-schema fails, then is skipped, and there is no else — so the document passes unconditionally. A document with no role and no permissions validates. Add "else": {"not": {"required": ["permissions"]}} to make the constraint bidirectional. Generation time for a Schemathesis fuzz run against a corrected 12-field schema dropped from roughly 40 seconds to 9 seconds once the contradictory branches were removed — the solver was spending time generating documents that satisfied both arms of an impossible condition.
Mistakes Senior Engineers Still Ship in Composite Schemas
The most common mistake is copy-pasting additionalProperties: false into every $defs component and assuming the root schema is locked down. It isn't. Each $defs entry is evaluated in its own applicator scope; a root object that references three definitions via allOf and adds no root-level unevaluatedProperties will accept arbitrary extra keys. This happens because schema authoring is distributed — the platform team writes the base definitions, feature teams extend them — and nobody owns the root constraint. The fix is a CI lint step using ajv compile --strict that flags any root schema missing unevaluatedProperties.
The second mistake is trusting validator defaults for draft version. ajv 8 defaults to draft-2020-12; jsonschema (Python) defaults to draft-07 unless you pass cls=jsonschema.Draft202012Validator. This means the same schema file behaves differently depending on which library runs it — a gap that surfaces when consumer fixtures drift from the provider schema and nobody catches it because both sides are technically "valid" under their respective validators. Standardize on a single draft version per contract boundary and encode it in the $schema keyword.
Three Myths That Let Widened Schemas Stay in Production
Myth 1: "We validate the schema, so we validate the data." Schema validation confirms structural conformance — it says nothing about semantic correctness, referential integrity, or whether a field combination is business-valid. A document with "status": "closed" and "closed_at": null passes most schemas. Pair schema validation with assertion layers (Great Expectations, Pydantic model validators, or SQL CHECK constraints) to cover the semantic layer. The step-by-step API schema validation workflow covers how to stack these layers without redundancy.
Myth 2: "anyOf means at least one branch is strict, so the union is strict." As Pattern 1 above shows, the union is only as strict as its least-restrictive branch at the point of evaluation. A document that partially satisfies one branch can smuggle fields past another branch's additionalProperties: false. Myth 3: "Property-based testing covers this automatically." Hypothesis generates valid inputs by default; it won't generate the adversarial keyword-interaction cases unless you explicitly build a strategy that targets boundary documents. Write a dedicated schema-boundary strategy using hypothesis-jsonschema with from_schema(bad_doc_variants) and assert that your canary documents fail — not just that random valid ones pass. Silent assertion passes on structurally bad data are the same failure mode at a different layer.
Schema keyword interactions are a specification problem masquerading as a tooling problem. Read the JSON Schema 2020-12 section on annotation collection and unevaluated keywords — it's about 1,500 words and will reframe every composite schema you've ever written. Then add a canary-document test suite that asserts known-bad payloads fail validation; a passing canary is your earliest warning that a keyword interaction has silently widened your boundary again.
Note: This article is for informational purposes only and is not a substitute for professional advice. If you need guidance on specific situations described in this article, consider consulting a qualified professional.