Skip to content

Use type array syntax for nullable types instead of anyOf #554

@ekarcnevets

Description

@ekarcnevets

Hi, thanks for your work on this package! I encountered a bug in openapi-generator today and traced it back to the following behaviour of zod-openapi. Not a bug here, this issue is more for awareness.

Problem

When converting Zod schemas with .nullable() to OpenAPI 3.1.0, zod-openapi produces:

{"anyOf": [{"type": "string"}, {"type": "null"}]}

This is valid OpenAPI 3.1.0, but it triggers a bug in openapi-generator (typescript-fetch generator) where path parameters in unrelated operations get incorrectly typed as string | null instead of string.

Proposed Solution

Use the equivalent type array syntax instead:

{"type": ["string", "null"]}

Both are valid OpenAPI 3.1.0 for expressing nullable types, but the type array syntax:

  1. Is more concise
  2. Doesn't trigger the openapi-generator bug
  3. Is arguably more idiomatic for simple nullable types

Example

Zod schema:

const schema = z.object({
  value: z.string().nullable()
});

Current output:

{
  "type": "object",
  "properties": {
    "value": {
      "anyOf": [{"type": "string"}, {"type": "null"}]
    }
  }
}

Proposed output:

{
  "type": "object",
  "properties": {
    "value": {
      "type": ["string", "null"]
    }
  }
}

Context

The openapi-generator bug has been reported at: OpenAPITools/openapi-generator#22427

The bug causes state corruption where anyOf containing {type: "null"} anywhere in components.schemas contaminates path parameter type inference for completely unrelated operations. The nullable schema doesn't even need to be referenced by the affected paths.

Workaround

Users can post-process the generated OpenAPI spec to convert anyOf nullable patterns:

function fixNullableAnyOf(obj) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (Array.isArray(obj)) return obj.map(fixNullableAnyOf);

  if (obj.anyOf && Array.isArray(obj.anyOf) && obj.anyOf.length === 2) {
    const types = obj.anyOf.map(item => item.type).filter(Boolean);
    if (types.length === 2 && types.includes('null')) {
      const nonNullType = types.find(t => t !== 'null');
      if (nonNullType && typeof nonNullType === 'string') {
        const { anyOf, ...rest } = obj;
        return { ...fixNullableAnyOf(rest), type: [nonNullType, 'null'] };
      }
    }
  }

  const result = {};
  for (const [key, value] of Object.entries(obj)) {
    result[key] = fixNullableAnyOf(value);
  }
  return result;
}

Alternatives Considered

  1. Configuration option - Add an option like nullableStyle: 'typeArray' | 'anyOf' to let users choose
  2. Keep current behavior - Wait for openapi-generator to fix the bug

Additional Context

  • OpenAPI 3.1.0 spec reference: Both anyOf and type arrays are valid for nullable types
  • This affects users generating TypeScript clients with openapi-generator
  • The anyOf syntax may still be appropriate for more complex unions (e.g., z.union([z.string(), z.number()]).nullable())

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions