Skip to content

feat: Map ZodTuple to a fixed-length array schema - #180

Merged
razor-x merged 1 commit into
mainfrom
claude/api-documentation-warnings-iq1n4k
Jul 28, 2026
Merged

feat: Map ZodTuple to a fixed-length array schema#180
razor-x merged 1 commit into
mainfrom
claude/api-documentation-warnings-iq1n4k

Conversation

@razor-x

@razor-x razor-x commented Jul 28, 2026

Copy link
Copy Markdown
Member

Tuples fell through to catchAllParser, so a z.tuple() field emitted an empty schema with no type. Downstream tooling then has nothing to work with — the Seam blueprint generator skips any property whose schema lacks a type, so tuple fields silently dropped out of the API documentation:

The created_between property for /seam/console/v1/timelines/get will not be documented since it does not define a type.
The enforced_setpoint_range_celsius property for device.properties.sensi_metadata will not be documented since it does not define a type.

OpenAPI 3.0 has no positional item schemas, so a tuple becomes a fixed-length array whose items accept every position's schema — a single schema when the positions agree, otherwise a oneOf. A .rest() type contributes its schema and drops maxItems, since a variadic tuple has no upper bound. prefixItems would describe the positions exactly, but it is OpenAPI 3.1 only, and consumers such as seam-connect emit openapi: "3.0.0" and lint the result, so emitting it would produce specs their tooling rejects.

Generated schemas, from the three fields added to the example app's /api/todo/add body:

Zod OpenAPI
z.tuple([z.string().datetime(), z.string().datetime()]) {type: "array", items: {type: "string", format: "date-time"}, minItems: 2, maxItems: 2}
z.tuple([z.number(), z.string()]) {type: "array", items: {oneOf: [{type: "number", format: "float"}, {type: "string"}]}, minItems: 2, maxItems: 2}
z.tuple([z.string()]).rest(z.string()) {type: "array", items: {type: "string"}, minItems: 1}

getTupleRest is added to the zod compat layer alongside the existing getTupleItems; both Zod 3 and Zod 4 keep the rest type at def.rest, so the existing getDef split covers it.

zod-to-ts already handled ZodTuple, so the generated TypeScript types are unchanged — this only affects the OpenAPI output.

Testing

  • yarn workspace nextlove build, typecheck, and test (8 tests) pass.
  • Example-app OpenAPI generation tests pass (9 tests), including three new assertions covering the homogeneous, heterogeneous, and variadic cases above.

Generated by Claude Code

Tuples fell through to catchAllParser, so a `z.tuple()` field emitted an empty
schema with no `type`. Downstream tooling then has nothing to work with: the
Seam blueprint generator skips any property whose schema lacks a type, so
tuple fields silently dropped out of the API documentation.

OpenAPI 3.0 has no positional item schemas, so a tuple becomes a fixed-length
array whose `items` accept every position's schema — a single schema when the
positions agree, otherwise a `oneOf`. A `.rest()` type contributes its schema
and drops `maxItems`, since a variadic tuple has no upper bound. `prefixItems`
would describe the positions exactly, but it is OpenAPI 3.1 only and would
produce specs that 3.0 tooling rejects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zayu5zwnZjsQXjNhDmsLF
@razor-x
razor-x merged commit c46056f into main Jul 28, 2026
2 checks passed
github-actions Bot pushed a commit that referenced this pull request Jul 28, 2026
# [3.3.0](v3.2.0...v3.3.0) (2026-07-28)

### Bug Fixes

* Publish to npm with trusted publishing (OIDC) ([#181](#181)) ([de0b569](de0b569))

### Features

* Map ZodTuple to a fixed-length array schema ([#180](#180)) ([c46056f](c46056f))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants