Discriminator keyword causes false validation failures even after mapping is stripped
Hello folks! I'd like to start a discussion on a gap I recently ran into with discriminator handling — building on previous work in this repo and its predecessor.
Background
OpenAPI's discriminator keyword is incompatible with JSON Schema validators like AJV in two ways:
discriminator.mapping is not supported by AJV (ajv-validator/ajv#2262).
- A
discriminator without an accompanying oneOf causes AJV to throw.
Both of these have been addressed in this repository through the cleanupDiscriminators transform (PRs #302, #320, #374), and in the predecessor repo swagger-mock-validator the problem was discussed in issues #51 and #56 (both closed as "not planned" at archival).
The problem I recently ran into
I recently bumped into a case where stripping the discriminator is not enough to make validation pass.
When a provider's OpenAPI spec uses discriminator.mapping alongside oneOf, the mapping encodes which concrete schema corresponds to each discriminator value — e.g.:
components:
schemas:
Animal:
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: type
mapping:
cat: '#/components/schemas/Cat'
dog: '#/components/schemas/Dog'
Cat:
type: object
properties:
type:
type: string
# ... cat-specific fields
Dog:
type: object
properties:
type:
type: string
# ... dog-specific fields
After cleanupDiscriminators strips discriminator.mapping (and potentially the entire discriminator object), AJV validates the response body against all oneOf branches simultaneously. If the concrete schemas are sufficiently similar — which is common in practice — more than one branch matches, causing AJV to fail the oneOf constraint ("must match exactly one schema").
The mapping that was stripped contained exactly the information needed to narrow validation to a single branch. By deleting it, we lose the ability to correctly route the instance to its branch.
Real-world context
This pattern is common in services that generate OpenAPI specs from code annotations — for example, JVM services using Swagger Core v3 (io.swagger.core.v3) with @Schema(discriminatorProperty = "type", discriminatorMapping = [...]) on sealed class hierarchies. The generated spec faithfully includes discriminator.mapping as part of the oneOf polymorphic schemas.
When such a spec is used in contract testing via openapi-pact-comparator, the current cleanupDiscriminators pass strips the mapping and leaves behind structurally near-identical oneOf branches with no way to differentiate them — resulting in false validation failures.
Possible enhancement: inject const/enum into sub-schemas from the mapping
One approach to fix this would be to use the discriminator.mapping information before discarding it, by injecting a const (or enum) constraint into each oneOf sub-schema for the discriminator property:
// Before stripping, transform:
// Cat schema: { type: object, properties: { type: { type: string }, ... } }
// Into:
// Cat schema: { type: object, properties: { type: { type: string, const: "cat" }, ... } }
This would allow AJV to correctly match exactly one oneOf branch for any given instance, without requiring AJV to understand discriminator.mapping itself. The transform would:
- Read
discriminator.propertyName and discriminator.mapping before cleanup.
- For each mapping entry
{ value → $ref }, resolve the referenced sub-schema.
- If the sub-schema's
properties[propertyName] does not already have a const or enum, inject const: <value> (or enum: [<value>]).
- Then proceed with the existing
cleanupDiscriminators pass.
What I'm unsure about and need input on
I don't have deep context on all the edge cases here, so I'd really value the maintainers' perspective before going further:
- Are there cases where this transform would produce incorrect results (e.g., specs where a discriminator value maps to multiple schemas, or where
const/enum injection would conflict with existing constraints)?
- Is the
allOf-flattening in flattenAllOf.ts expected to already address some of these cases? Does the interaction between the two transforms matter?
- Is there a reason the current approach chose to be purely subtractive (strip only) rather than additive (transform before stripping)?
- Are there other patterns (e.g.,
anyOf instead of oneOf, inline schemas instead of $refs) that would need to be handled?
I may be able to contribute an implementation, but I can't commit to that timeline at this point — I'm raising this primarily to document the gap and get input on the right direction.
References
Discriminator keyword causes false validation failures even after mapping is stripped
Hello folks! I'd like to start a discussion on a gap I recently ran into with discriminator handling — building on previous work in this repo and its predecessor.
Background
OpenAPI's
discriminatorkeyword is incompatible with JSON Schema validators like AJV in two ways:discriminator.mappingis not supported by AJV (ajv-validator/ajv#2262).discriminatorwithout an accompanyingoneOfcauses AJV to throw.Both of these have been addressed in this repository through the
cleanupDiscriminatorstransform (PRs #302, #320, #374), and in the predecessor repo swagger-mock-validator the problem was discussed in issues #51 and #56 (both closed as "not planned" at archival).The problem I recently ran into
I recently bumped into a case where stripping the discriminator is not enough to make validation pass.
When a provider's OpenAPI spec uses
discriminator.mappingalongsideoneOf, the mapping encodes which concrete schema corresponds to each discriminator value — e.g.:After
cleanupDiscriminatorsstripsdiscriminator.mapping(and potentially the entirediscriminatorobject), AJV validates the response body against alloneOfbranches simultaneously. If the concrete schemas are sufficiently similar — which is common in practice — more than one branch matches, causing AJV to fail theoneOfconstraint ("must match exactly one schema").The
mappingthat was stripped contained exactly the information needed to narrow validation to a single branch. By deleting it, we lose the ability to correctly route the instance to its branch.Real-world context
This pattern is common in services that generate OpenAPI specs from code annotations — for example, JVM services using Swagger Core v3 (
io.swagger.core.v3) with@Schema(discriminatorProperty = "type", discriminatorMapping = [...])on sealed class hierarchies. The generated spec faithfully includesdiscriminator.mappingas part of theoneOfpolymorphic schemas.When such a spec is used in contract testing via
openapi-pact-comparator, the currentcleanupDiscriminatorspass strips the mapping and leaves behind structurally near-identicaloneOfbranches with no way to differentiate them — resulting in false validation failures.Possible enhancement: inject
const/enuminto sub-schemas from the mappingOne approach to fix this would be to use the
discriminator.mappinginformation before discarding it, by injecting aconst(orenum) constraint into eachoneOfsub-schema for the discriminator property:This would allow AJV to correctly match exactly one
oneOfbranch for any given instance, without requiring AJV to understanddiscriminator.mappingitself. The transform would:discriminator.propertyNameanddiscriminator.mappingbefore cleanup.{ value → $ref }, resolve the referenced sub-schema.properties[propertyName]does not already have aconstorenum, injectconst: <value>(orenum: [<value>]).cleanupDiscriminatorspass.What I'm unsure about and need input on
I don't have deep context on all the edge cases here, so I'd really value the maintainers' perspective before going further:
const/enuminjection would conflict with existing constraints)?allOf-flattening inflattenAllOf.tsexpected to already address some of these cases? Does the interaction between the two transforms matter?anyOfinstead ofoneOf, inline schemas instead of$refs) that would need to be handled?I may be able to contribute an implementation, but I can't commit to that timeline at this point — I'm raising this primarily to document the gap and get input on the right direction.
References
mappingkeyword whendiscriminatoris usedoneOfwith nestedallOfdiscriminator.mappingsupport