Skip to content

Improve discriminator handling: inject const/enum from mapping before stripping #554

Description

@argos83

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:

  1. discriminator.mapping is not supported by AJV (ajv-validator/ajv#2262).
  2. 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:

  1. Read discriminator.propertyName and discriminator.mapping before cleanup.
  2. For each mapping entry { value → $ref }, resolve the referenced sub-schema.
  3. If the sub-schema's properties[propertyName] does not already have a const or enum, inject const: <value> (or enum: [<value>]).
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    smartbear-supportedSmartBear engineering team will support this issue. See https://docs.pact.io/help/smartbear

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions