DEV Community

ITFabers
ITFabers

Posted on

Generating request payloads for `oneOf` with `discriminator` in OpenAPI, without generating garbage

I spent most of a weekend on a bug that boiled down to one wrong assumption: that oneOf and discriminator are basically the same feature. They're not, and if your payload generator treats them as interchangeable, it'll produce technically-valid JSON that fails validation on the server every single time.

Here's the setup that broke things. A payments API with a PaymentMethod schema:

PaymentMethod:
  oneOf:
    - $ref: '#/components/schemas/CardPayment'
    - $ref: '#/components/schemas/BankTransfer'
    - $ref: '#/components/schemas/PayPalPayment'
  discriminator:
    propertyName: type
    mapping:
      card: '#/components/schemas/CardPayment'
      bank_transfer: '#/components/schemas/BankTransfer'
      paypal: '#/components/schemas/PayPalPayment'
Enter fullscreen mode Exit fullscreen mode

Naive generators do one of two things wrong. First mistake: pick a random branch from oneOf, build the fields, and forget to set type at all — because nothing in the branch schema itself says the field is required if the discriminator lives at the parent level. Second mistake: set type to the schema's own name (CardPayment) instead of the mapped value (card), because the mapping block is easy to skip if you're just walking oneOf and resolving refs.

Both produce a payload that parses as valid JSON, passes basic schema checks in a lot of naive validators, and then gets rejected by the actual API because the discriminator value doesn't match anything the server recognizes.

The fix is to treat discriminator resolution as its own pass, separate from picking a oneOf branch:

  1. Resolve the oneOf branch first — for coverage you want to generate a payload per branch, not just one.
  2. For each branch, look up whether it's referenced in discriminator.mapping. If there's no explicit mapping, fall back to the implicit rule: the discriminator value is the schema's $ref name.
  3. Force-set propertyName on the generated object to that resolved value, overwriting whatever the branch's own field generation would have produced (a naive string generator will happily fill type with "lorem ipsum" if you let it).
  4. Only then generate the rest of the branch's required fields.

The other trap is allOf + discriminator, which is the more common real-world shape — a base object with shared fields, extended per-variant:

CardPayment:
  allOf:
    - $ref: '#/components/schemas/PaymentMethod'
    - type: object
      properties:
        card_number: { type: string }
        expiry: { type: string }
Enter fullscreen mode Exit fullscreen mode

Here the discriminator property doesn't even live in the branch schema — it's on the base. If your generator resolves allOf by shallow-merging properties, this works fine. If it resolves refs lazily or caches merged schemas by name, you can end up with all three payment variants sharing one mutated base object and one clobbering another's discriminator value. That one took longer to find than I'd like to admit — it only showed up when generating multiple variants in the same test run, not in isolation.

I ended up writing this exact resolution order into apitestgen.dev after finding it the hard way on a real spec. If you're building anything that walks OpenAPI schemas to synthesize data — test payloads, mocks, fixtures — discriminator resolution deserves its own explicit step. Don't let it fall out of your oneOf branch-picking logic as a side effect.

Top comments (0)