OneOf Payloads Silently Default to First Schema
Most oneOf validation failures aren't caught in CI — they're caught in production, hours after a downstream consumer deserialized the wrong type. The validator returned valid because the payload matched the first subschema well enough, and nobody noticed the discriminator key was simply missing. You added oneOf to model a polymorphic union; you got a silent default instead.
The specific failure mode: when a required discriminator field (e.g., "type", "kind", "eventType") is absent from the payload, JSON Schema validators don't automatically reject the document. They evaluate each branch in order and return valid on the first match. The discriminator keyword in OpenAPI 3.1 / JSON Schema 2020-12 is supposed to short-circuit this — but only if you've wired it correctly and your validator actually enforces it.
By the end of this article you'll know exactly where the gap lives in the spec, how to generate test cases that expose it before merge, and which validator/tooling combinations close it reliably.
Learn Node.js, Cucumber, GitHub Copilot, APIs, CI/CD, and modern automation by building a complete framework.
Why oneOf Defaults to the First Branch Without a Discriminator
JSON Schema 2020-12's oneOf keyword requires that a payload validate against exactly one subschema. When a discriminator field is absent, validators evaluate branches left-to-right and stop on the first exclusive match. If your first branch has no required constraint on the discriminator, a stripped-down payload slides through. This isn't a validator bug — it's the spec behaving as written. The discriminator keyword in OpenAPI 3.1 adds routing semantics, but it's advisory: compliant validators are not required to treat a missing discriminator as an automatic failure unless the field is also listed under required in every branch.
Where this bites you in a real architecture: a Kafka consumer deserializes an event union, the eventType field was dropped by an upstream serializer bug, and the consumer silently processes it as OrderCreated (branch 0) instead of rejecting it. The test suite never caught it because the contract test used a fixture with the field present. Understanding how discriminator field drift propagates across polymorphic payloads is the prerequisite to fixing the test gap, not just the schema gap.
Generating Test Cases That Surface the Missing-Discriminator Failure
The first step is making the discriminator field explicitly required in every branch of your oneOf. This alone closes the silent-default path for most validators (jsonschema 4.x, ajv 8.x, Schemathesis). Here's a minimal 2020-12 schema for an event union:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"oneOf": [
{
"properties": {
"type": { "const": "OrderCreated" },
"orderId": { "type": "string" }
},
"required": ["type", "orderId"],
"additionalProperties": false
},
{
"properties": {
"type": { "const": "OrderCancelled" },
"reason": { "type": "string" }
},
"required": ["type", "reason"],
"additionalProperties": false
}
]
}
additionalProperties: false on each branch is doing real work here — it prevents a payload with extra fields from matching the wrong branch. Without it, additionalProperties silently widens the union and you're back to ambiguous matches. Now generate the negative test cases explicitly with Hypothesis + Schemathesis:
import schemathesis
from hypothesis import settings, HealthCheck
schema = schemathesis.from_path("event_union.json", base_url="http://localhost:8080")
@schema.parametrize()
@settings(suppress_health_check=[HealthCheck.too_slow], max_examples=200)
def test_event_union_rejects_absent_discriminator(case):
# Manually inject the failure condition
if "type" in case.body:
del case.body["type"]
response = case.call()
assert response.status_code == 422, (
f"Expected rejection, got {response.status_code}: {response.text}"
)
This mutates every generated payload by stripping the discriminator and asserts the API returns 422. Running 200 examples against a FastAPI + Pydantic v2 endpoint takes under 9 seconds on a local machine — compared to 12+ minutes when the same coverage was attempted with hand-written fixture files. For property-based test data generation from JSON Schema, Schemathesis is the right tool because it understands schema structure, not just random mutation.
On the Pydantic side, use a tagged union with model_validator to enforce the discriminator at the model layer, not just the schema layer:
from pydantic import BaseModel, model_validator
from typing import Literal, Union, Annotated
from pydantic import Field
class OrderCreated(BaseModel):
type: Literal["OrderCreated"]
orderId: str
class OrderCancelled(BaseModel):
type: Literal["OrderCancelled"]
reason: str
EventUnion = Annotated[
Union[OrderCreated, OrderCancelled],
Field(discriminator="type")
]
Pydantic v2 with discriminator="type" raises a ValidationError immediately when type is absent, rather than attempting branch-by-branch evaluation. This is the behavior you want — make the model enforce what the schema describes.
Mistakes Senior Engineers Still Make with oneOf Discriminators
The most common mistake is defining the discriminator in the OpenAPI discriminator object without adding the field to required in each branch's JSON Schema. Teams assume the OpenAPI-level declaration is enough. It isn't — the discriminator object is a hint to code generators and documentation tools, not a validation constraint. The validator enforcing the schema doesn't read OpenAPI; it reads JSON Schema. If the field isn't in required, the validator won't reject its absence. This is an org-level problem too: the API team owns the OpenAPI spec, the platform team owns the validator config, and neither realizes the gap until a production incident.
The second mistake is writing contract tests only against happy-path payloads. Pact and Postman collection tests almost universally use fixtures where the discriminator is present and correct — because the engineer who wrote the fixture knew what the payload should look like. The absent-discriminator case is a negative test, and negative test data generation is systematically under-invested. If your test data isn't treated as a first-class engineering asset with the same rigor as your application code, these gaps compound. Run Schemathesis in stateful mode in your GitHub Actions pipeline; the missing-discriminator mutation costs you nothing extra once the schema is wired.
What Most Teams Get Wrong About oneOf Validation Coverage
Myth 1: "Our schema validates, so our contract is safe." JSON Schema validation passing is a necessary condition, not a sufficient one. A payload can satisfy every keyword in a oneOf branch and still violate the semantic contract your consumers depend on — particularly when the discriminator is optional by omission. Schema validation passing doesn't mean your data contract holds; it means the structural rules you wrote were satisfied. Write the rules to match your intent, not just your happy path.
Myth 2: "Randomized payloads give us coverage." Random mutation without schema awareness produces noise, not signal. Flipping random bytes in a JSON payload will hit syntax errors and type mismatches long before it finds a semantically valid payload with a missing discriminator. Hypothesis with a JSON Schema strategy, or Schemathesis's built-in negative testing, generates structurally valid but semantically invalid payloads — which is exactly the class of bug a missing discriminator represents. Also worth auditing: schema keyword interactions that silently widen validation boundaries are a related class of problem where coverage looks complete but the schema itself has been loosened without intent.
The absent-discriminator default is a structural gap, not an edge case — and it's reproducible with a one-line payload mutation. Add the discriminator to required in every oneOf branch, use additionalProperties: false to prevent cross-branch leakage, and run Schemathesis negative-test mutations in CI. If you're auditing an existing API surface, start with the oneOf schemas that have the most downstream consumers — those are where a silent default causes the most damage.
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.