Zod: constraint-only oneOf branches with sibling properties generate zod.unknown() and lose validation
#3,780 opened on 2026/07/28
Repository metrics
- Stars
- (6,272 個のスター)
- PR merge metrics
- (PR metrics pending)
説明
Description
A valid OpenAPI 3.1 object schema can define property types in a common properties block and use constraint-only oneOf branches to define conditional required or forbidden fields.
Orval generates these branches as zod.unknown(), causing the generated validator to accept values rejected by the OpenAPI schema.
Current configuration
Output client: zod
orval.config.ts
import { defineConfig } from 'orval';
export default defineConfig({
reproduction: {
input: './openapi.yaml',
output: {
client: 'zod',
target: './generated.ts',
override: {
zod: {
version: 3,
generate: {
body: true,
response: false,
query: false,
param: false,
header: false,
},
},
},
},
},
});
Generation command:
pnpm exec orval --config ./orval.config.ts
Environment
System:
OS: Windows 11 10.0.26200
npmPackages:
@tanstack/vue-query: ^5.83.1 => 5.85.6
axios: ^1.18.0 => 1.18.0
msw: ^2.11.5 => 2.11.5
orval: ^8.23.0 => 8.23.0
vue: ^3.5.35 => 3.5.35
zod: ^3.25.76 => 3.25.76
Swagger
openapi: 3.1.0
info:
title: Constraint-only oneOf reproduction
version: 1.0.0
paths:
/examples:
post:
operationId: createExample
responses:
'204':
description: No Content
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateExampleRequest'
components:
schemas:
CreateExampleRequest:
type: object
oneOf:
- title: AB
required:
- A
- B
not:
anyOf:
- required:
- X
- required:
- Y
- title: XY
required:
- X
- Y
properties:
A:
type: string
B:
type: integer
X:
type: string
Y:
type: integer
The document was validated with the command requested by the issue template:
npx --yes @scalar/cli document validate ./openapi.yaml
Result:
Matches the OpenAPI specification (OpenAPI 3.1)
File validated in 80 ms
The schema describes two mutually exclusive cases:
AB:AandBare required;XandYare forbidden.XY:XandYare required;AandBremain optional.
Generated Zod
Orval 8.23.0 generates:
export const CreateExampleBody = zod.union([zod.unknown(),zod.unknown()]).and(zod.object({
"A": zod.string().optional(),
"B": zod.number().int().optional(),
"X": zod.string().optional(),
"Y": zod.number().int().optional()
}))
Runtime reproduction:
const cases = {
empty: {},
incompleteXY: {
X: 'x',
},
validAB: {
A: 'a',
B: 1,
},
validXY: {
X: 'x',
Y: 1,
},
invalidMixed: {
A: 'a',
B: 1,
X: 'x',
},
};
console.log(
Object.fromEntries(
Object.entries(cases).map(([name, value]) => [
name,
CreateExampleBody.safeParse(value).success,
]),
),
);
Actual result:
{
"empty": true,
"incompleteXY": true,
"validAB": true,
"validXY": true,
"invalidMixed": true
}
The generated schema incorrectly accepts:
empty, which matches neither branch.incompleteXY, becauseYis missing.invalidMixed, because theABbranch forbidsX, while theXYbranch requiresY.
Expected Zod
The generated schema should preserve the common property types and the branch constraints.
A semantically equivalent Zod schema for JSON payloads would be:
const AB = zod.object({
A: zod.string(),
B: zod.number().int(),
X: zod.never().optional(),
Y: zod.never().optional(),
});
const XY = zod.object({
A: zod.string().optional(),
B: zod.number().int().optional(),
X: zod.string(),
Y: zod.number().int(),
});
export const CreateExampleBody = zod.union([AB, XY]);
Expected result:
{
"empty": false,
"incompleteXY": false,
"validAB": true,
"validXY": true,
"invalidMixed": false
}
The expected results were verified both with the Zod schema above and with Ajv 8.20.0 using its JSON Schema 2020-12 validator.
If Orval cannot preserve a supported OpenAPI constraint, generation should report an unsupported constraint instead of silently replacing the branch with zod.unknown().