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'
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:
- Resolve the
oneOfbranch first — for coverage you want to generate a payload per branch, not just one. - 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$refname. - Force-set
propertyNameon the generated object to that resolved value, overwriting whatever the branch's own field generation would have produced (a naive string generator will happily filltypewith"lorem ipsum"if you let it). - 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 }
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)