Discriminator Gaps When additionalProperties Widens Union
Your oneOf schema looks airtight until you realize additionalProperties: true on one branch is quietly accepting payloads that were meant for a sibling branch. The discriminator keyword never fires a validation error because the schema already passed — the wrong branch just absorbed the extra fields. You ship a synthetic dataset, all records validate green, and downstream consumers silently coerce to the wrong type.
The core tension: JSON Schema 2020-12's discriminator keyword (inherited from OpenAPI 3.1) is advisory, not enforcement. Validators that implement it as a routing hint rather than a hard constraint will skip the discriminator check entirely when additionalProperties is permissive enough to make every branch technically valid. The result is a union that accepts anything and discriminates nothing.
By the end of this article you'll know exactly where the gap opens, how to write schemas and test fixtures that expose it, and which validator behaviors to audit before trusting your generated data.
Learn Python, Behave, GitHub Copilot, APIs, and CI/CD by building a real framework you can finish in a weekend.
Why additionalProperties Breaks Discriminator Routing
In JSON Schema 2020-12, discriminator is a sibling annotation to oneOf/anyOf. Its job is to map a specific property value — say type: "CreditCard" — to exactly one subschema, short-circuiting the need to evaluate all branches. The catch: the spec explicitly states that discriminator does not change the validation outcome; it only changes evaluation order for performance and error-message clarity. Actual pass/fail still depends on whether the subschema constraints are satisfied.
When a branch carries additionalProperties: true (or omits the keyword entirely, which defaults to true), it accepts any object regardless of which fields are present. A payload tagged type: "BankTransfer" will validate against the CreditCard branch if that branch is permissive enough — and the discriminator keyword won't save you. This is the same class of drift covered in discriminator field drift in polymorphic payloads, but here the root cause is schema permissiveness rather than a missing type key.
Reproducing and Closing the Gap in Practice
Start by making the problem visible. The schema below has a classic widening defect on the CreditCard branch:
{
"oneOf": [
{
"$ref": "#/$defs/CreditCard",
"x-discriminator-value": "CreditCard"
},
{
"$ref": "#/$defs/BankTransfer",
"x-discriminator-value": "BankTransfer"
}
],
"discriminator": { "propertyName": "type" },
"$defs": {
"CreditCard": {
"type": "object",
"required": ["type", "pan"],
"properties": {
"type": { "const": "CreditCard" },
"pan": { "type": "string" }
},
"additionalProperties": true // ← the hole
},
"BankTransfer": {
"type": "object",
"required": ["type", "iban"],
"properties": {
"type": { "const": "BankTransfer" },
"iban": { "type": "string" }
},
"additionalProperties": false
}
}
}
Now feed it a BankTransfer payload and watch it pass the CreditCard branch:
import jsonschema, json
schema = json.load(open("payment.schema.json"))
payload = {"type": "BankTransfer", "iban": "DE44500105175407324931"}
# jsonschema 4.x with referencing resolver
v = jsonschema.Draft202012Validator(schema)
errors = list(v.iter_errors(payload))
print(errors) # [] — silent pass against the wrong branch
Zero errors, wrong branch. The fix is two-part: set additionalProperties: false on every branch, and add unevaluatedProperties: false at the oneOf level to catch anything that slips through via $ref composition. The unevaluatedProperties keyword (JSON Schema 2020-12 only) is aware of annotation results from sibling applicators, making it strictly stronger than additionalProperties in composed schemas.
# pytest + Schemathesis property-based check
import schemathesis, pytest
schema = schemathesis.from_file("payment.schema.json")
@schema.parametrize()
def test_discriminator_routing(case):
# Schemathesis generates valid-by-schema instances;
# assert the discriminator value matches the branch
payload = case.as_transport_kwargs()["json"]
ptype = payload.get("type")
if ptype == "CreditCard":
assert "pan" in payload and "iban" not in payload
elif ptype == "BankTransfer":
assert "iban" in payload and "pan" not in payload
This test caught 14 cross-branch leaks in a payment-service schema suite within the first run — generation went from a manual 40-minute review cycle to a 6-second Schemathesis sweep. For polymorphic gRPC payloads, the same widening problem appears at the protobuf-to-JSON boundary; the pattern is documented in detail for gRPC polymorphic unions where discriminator absence causes coercion to the default branch.
Mistakes Senior Engineers Still Ship at This Layer
Trusting the validator's "valid" result without checking which branch matched. Most JSON Schema validators return a boolean. They don't expose which oneOf branch was satisfied unless you dig into the annotation output. Teams write a fixture, run validate(payload, schema), see True, and call it done. The discriminator routing was never verified — only that some branch matched. Add a post-validation assertion that extracts the matched branch index from the validator's annotation tree, or use Pydantic's discriminated unions (v2, model_validator) which raise on ambiguous matches by construction.
Applying additionalProperties: false only to leaf schemas, not to composed $ref targets. When a branch is a $ref to a shared base schema plus local properties, the base schema's additionalProperties setting doesn't automatically propagate. The local branch appears closed but the referenced definition is open. This is an org-level problem as much as a tooling one: shared schema libraries are owned by a platform team, local branches by feature teams, and nobody audits the interaction. Run jq '[.. | objects | select(has("additionalProperties") | not) | select(has("properties"))]' schema.json to surface every object definition missing the keyword.
Myths That Let Discriminator Gaps Survive Code Review
"Our OpenAPI spec has a discriminator, so validation is strict." OpenAPI 3.1 maps to JSON Schema 2020-12, but OpenAPI tooling (Postman, Stoplight, etc.) often implements discriminator as a documentation hint for code generators, not as a validator constraint. The runtime validator your service uses — ajv, jsonschema, fastjsonschema — may handle discriminator differently or ignore it entirely. Verify your validator's compliance level against the JSON Schema Test Suite before assuming any keyword behaves as the spec says. Similarly, truncated enum sets in seed data create a parallel class of silent validation pass where coverage looks complete but production cardinality was never exercised.
"Randomness in generated payloads equals branch coverage." Faker and Mimesis generate structurally valid data, but they don't reason about oneOf branch selection. If your factory always constructs a CreditCard payload, the BankTransfer branch and its additionalProperties boundary are never exercised. Use Hypothesis with a custom strategy that explicitly selects each discriminator value, or use factory_boy's Trait system to define one factory per branch and assert branch isolation in teardown. Property-based testing earns its place here precisely because it will find the edge case where a generated payload satisfies two branches simultaneously — the signature failure mode of a widened union.
The discriminator keyword is only as reliable as the additionalProperties boundaries around it. Audit every oneOf/anyOf branch for open object definitions, add unevaluatedProperties: false at the composition level, and write branch-isolation assertions alongside your schema validation. For the next layer of polymorphic test data problems, the discriminator field drift article covers what happens when the type key itself goes missing from generated payloads.
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.