orval-labs/orval

Zod: constraint-only oneOf branches with sibling properties generate zod.unknown() and lose validation

Offen

#3.780 geöffnet am 28.07.2026

 (4 Kommentare) (0 Reaktionen) (0 zugewiesene Personen)TypeScript (648 Forks)auto 404
help wantedzod

Repository-Metriken

Stars
 (6.272 Sterne)
PR-Merge-Metriken
 (PR-Metriken ausstehend)

Beschreibung

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: A and B are required; X and Y are forbidden.
  • XY: X and Y are required; A and B remain 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, because Y is missing.
  • invalidMixed, because the AB branch forbids X, while the XY branch requires Y.

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().

Contributor Guide