Skip to content

feat(zod): add generateMeta to attach .meta() to component schemas (zod v4) - #3469

Merged
melloware merged 2 commits into
orval-labs:masterfrom
z4o4z:feat/zod-meta
May 28, 2026
Merged

feat(zod): add generateMeta to attach .meta() to component schemas (zod v4)#3469
melloware merged 2 commits into
orval-labs:masterfrom
z4o4z:feat/zod-meta

Conversation

@z4o4z

@z4o4z z4o4z commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an opt-in override.zod.generateMeta (default false). When enabled on zod v4, each generated component schema gets registry metadata via .meta():

// override: { zod: { generateMeta: true } }
export const Pet = zod
  .object({ id: zod.number(), name: zod.string().describe('the pet name') })
  .meta({ id: 'Pet', description: 'A pet in the store', deprecated: true });
  • id is the schema name (always); description and deprecated are included only when the OpenAPI schema provides them.
  • This makes schemas self-describing and lets z.toJSONSchema() reference them by id (#/$defs/Pet), round-tripping the component structure.

Design

  • Generator fold: a top-level-only emitMeta rule folds the schema's trailing .describe(...) into a single .meta({ id, description?, deprecated? }) on zod v4; nested property descriptions keep .describe(). Handled at both generator exit points (multi-type union + main).
  • Ordering matters: .meta() is emitted last. Verified against zod 4.3.6 that .meta({id}).describe(...) wraps the schema in a $ref indirection, whereas .describe(...).meta({id}) (and a lone .meta) stay flat.
  • Scope = component schemas only (named exports — schemas: { type: 'zod' } or client: 'zod' + generateReusableSchemas). Operation wrapper schemas are intentionally left untouched, so zod's global-registry ids never collide.
  • zod v3: no .meta() exists — the option is a silent no-op and descriptions still emit via .describe().

Test plan

  • Generator unit tests: v4 folds id+description+deprecated into one .meta(); id-only when no description/deprecated; nested keeps .describe(); v3 falls back to .describe(); emitMeta off → no .meta(); multi-type (type-array) top-level schema gets meta
  • Parser unit: ['meta', {...}].meta({ id: 'X', description: '...', deprecated: true })
  • Wiring tests (write-zod-specs): config → .meta() on zod v4; .describe() (no .meta) on zod v3
  • Generated v4 output type-checks under strict; z.toJSONSchema() surfaces id / description / deprecated (nested description preserved)
  • lint, typecheck, and full suites pass — core 1933, zod 188, orval 119
  • Docs: new ### generateMeta section under override.zod

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added zod.generateMeta (default: false) to optionally include registry metadata (id, description, deprecated) on generated Zod component schemas (emits .meta() on Zod v4, falls back to .describe() on v3).
  • Documentation
    • Docs updated to describe the new option, its defaults, and cross-version behavior.
  • Tests
    • Expanded tests and updated defaults to validate generateMeta behavior and regressions.
  • Types
    • Public configuration types updated to expose the new option.

Review Change Stack

Copilot AI review requested due to automatic review settings May 27, 2026 17:47
@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5f9bf194-2127-482f-a9dd-38392634f375

📥 Commits

Reviewing files that changed from the base of the PR and between cea9ac7 and 547ab5a.

📒 Files selected for processing (13)
  • docs/content/docs/reference/configuration/output.mdx
  • packages/angular/src/http-client.test.ts
  • packages/angular/src/http-resource.test.ts
  • packages/core/src/test-utils/context.ts
  • packages/core/src/types.ts
  • packages/mock/src/faker/getters/combine.test.ts
  • packages/orval/src/reusable-schemas.ts
  • packages/orval/src/utils/options.ts
  • packages/orval/src/write-zod-specs.test.ts
  • packages/orval/src/write-zod-specs.ts
  • packages/solid-start/src/index.test.ts
  • packages/zod/src/index.ts
  • packages/zod/src/zod.test.ts
✅ Files skipped from review due to trivial changes (3)
  • packages/mock/src/faker/getters/combine.test.ts
  • packages/angular/src/http-resource.test.ts
  • docs/content/docs/reference/configuration/output.mdx
🚧 Files skipped from review as they are similar to previous changes (9)
  • packages/angular/src/http-client.test.ts
  • packages/solid-start/src/index.test.ts
  • packages/orval/src/utils/options.ts
  • packages/core/src/types.ts
  • packages/orval/src/write-zod-specs.ts
  • packages/zod/src/index.ts
  • packages/orval/src/reusable-schemas.ts
  • packages/orval/src/write-zod-specs.test.ts
  • packages/zod/src/zod.test.ts

📝 Walkthrough

Walkthrough

Adds a generateMeta option to Zod generation that, when enabled, emits Zod v4 .meta({ id, description?, deprecated? }) on generated component schemas (Zod v3 falls back to .describe()); threads the option through types, option normalization, Orval wiring, code generation, parsing, tests, and docs.

Changes

Zod v4 Metadata Emission

Layer / File(s) Summary
Type definitions and configuration defaults
packages/core/src/types.ts, packages/core/src/test-utils/context.ts, packages/orval/src/utils/options.ts, packages/angular/src/http-client.test.ts, packages/angular/src/http-resource.test.ts, packages/mock/src/faker/getters/combine.test.ts, packages/solid-start/src/index.test.ts
generateMeta added to ZodOptions (optional) and NormalizedZodOptions (required); default false threaded into test helpers and normalization for global and per-operation settings.
Orval option threading through schema generation paths
packages/orval/src/write-zod-specs.ts, packages/orval/src/reusable-schemas.ts
Add generateMeta to WriteZodOutputOptions and GenerateReusableSchemaSetOptions; forward the flag into inline and reusable schema generation and file-based writing so codegen can emit .meta() when enabled.
Core Zod v4 metadata generation logic
packages/zod/src/index.ts
Introduce rules.emitMeta?: boolean, implement pushDescriptionOrMeta to append .meta({ id, description?, deprecated? }) on Zod v4 or .describe() on Zod v3; call helper from main and union exit points; extend parser to handle meta function entries with stable field ordering.
Documentation and comprehensive test coverage
docs/content/docs/reference/configuration/output.mdx, packages/orval/src/write-zod-specs.test.ts, packages/zod/src/zod.test.ts
Document override.zod.generateMeta semantics, Zod version behavior, and toJSONSchema() impacts; add regression tests asserting v4 .meta() emission, v3 fallback, modifier ordering, and empty-description handling for inline and reusable schemas.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • orval-labs/orval#3465: Overlaps with edits to Zod schema writing pipeline and wiring override.zod.generateMeta through inline/reusable generation paths.

Suggested labels

enhancement

Suggested reviewers

  • melloware
  • snebjorn

Poem

🐰 I nibbled docs and code today,
A meta flag to lead the way,
For Zod v4 it leaves a trace,
An {id} tucked in the right place,
Hop—tests cheer, and types say "hooray!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main feature being added: a new generateMeta option for Zod v4 that attaches .meta() to component schemas.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@melloware melloware added the zod Zod schema client related issue label May 27, 2026
@melloware melloware added this to the 8.14.0 milestone May 27, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds optional Zod v4 registry metadata emission for generated component schemas by emitting .meta({ id, description?, deprecated? }), while keeping Zod v3 behavior as .describe(...).

Changes:

  • Introduces generateMeta/emitMeta option plumbing from config → generators → reusable schema generation.
  • Updates Zod schema generation to emit top-level .meta(...) (Zod v4) and adds parsing support for the meta function in definitions.
  • Adds tests and documentation for the new configuration flag.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/zod/src/zod.test.ts Adds unit tests for .meta() emission/parsing behavior across v3/v4 and schema shapes.
packages/zod/src/index.ts Implements .meta() emission (emitMeta) and parsing logic for meta in generated chains.
packages/orval/src/write-zod-specs.ts Plumbs generateMeta from output options into schema generation calls.
packages/orval/src/write-zod-specs.test.ts Adds integration tests ensuring generateMeta behaves correctly for v3 vs v4.
packages/orval/src/utils/options.ts Normalizes the new generateMeta option into normalized output options.
packages/orval/src/reusable-schemas.ts Passes generateMeta into reusable schema set generation (as emitMeta).
packages/core/src/types.ts Adds generateMeta to (normalized) Zod options types.
packages/core/src/test-utils/context.ts Updates test context defaults to include generateMeta: false.
docs/content/docs/reference/configuration/output.mdx Documents generateMeta and its behavior/limitations.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/zod/src/index.ts
Comment thread packages/zod/src/index.ts Outdated
Comment thread packages/zod/src/index.ts
Comment thread packages/orval/src/write-zod-specs.test.ts
Comment thread packages/orval/src/write-zod-specs.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
packages/orval/src/write-zod-specs.test.ts (1)

625-639: ⚡ Quick win

Consider more flexible assertions for metadata field order.

The exact string match for .meta({ id: 'Pet', description: 'A pet', deprecated: true }) will break if the field order changes (e.g., if the generator reorders fields alphabetically or changes quote style). Consider breaking this into separate assertions:

-    expect(result).toContain(
-      ".meta({ id: 'Pet', description: 'A pet', deprecated: true })",
-    );
+    expect(result).toContain('.meta(');
+    expect(result).toContain("id: 'Pet'");
+    expect(result).toContain("description: 'A pet'");
+    expect(result).toContain('deprecated: true');

This would make the test more resilient to formatting changes while still verifying all required fields are present.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/orval/src/write-zod-specs.test.ts` around lines 625 - 639, The test
currently asserts an exact substring match for the generated .meta object which
is brittle to field order/formatting; update the assertion in the test case that
uses generateZodSchemasInline(metaBuilder(), output) so it instead checks for
the presence of the individual metadata properties (e.g., that the result
contains "id: 'Pet'", "description: 'A pet'", and "deprecated: true") or use a
regex that tolerates any key order/spacing; locate the test block referencing
createOutputOptions(), zodOverride.generateMeta, metaBuilder(), and
generateZodSchemasInline and replace the strict
expect(result).toContain(".meta({...})") with separate contains assertions or an
order-agnostic regex check.
packages/zod/src/zod.test.ts (2)

7919-8063: ⚡ Quick win

Consider adding tests for partial metadata and edge cases.

The test coverage is solid for the main scenarios, but a few additional test cases would strengthen confidence:

  1. Partial metadata: Test when only description is present (no deprecated), and vice versa
  2. Modifier ordering: Verify .meta() is placed after modifiers like .optional(), .nullable(), .default() to ensure the correct chain order
  3. Additional schema types: Test top-level oneOf, allOf, anyOf with meta to ensure it works beyond objects and multi-type unions
  4. Edge cases: Empty/whitespace-only description strings

The current tests thoroughly cover the primary happy paths and the v3/v4 split, so this is a nice-to-have rather than blocking.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/zod/src/zod.test.ts` around lines 7919 - 8063, Add additional unit
tests in packages/zod/src/zod.test.ts that exercise partial metadata and edge
cases: create cases where only description exists (no deprecated) and only
deprecated exists to assert def.functions contains the expected meta payload via
generateZodValidationSchemaDefinition and parseZodValidationSchemaDefinition;
add tests that ensure .meta() appears after modifiers by building schemas with
optional/nullable/default and asserting parsed.zod shows modifiers before
".meta(...)" (use the same def.functions and parsed.zod checks as existing
tests); add top-level oneOf/allOf/anyOf schemas to confirm meta is emitted for
those constructs; and add tests for empty or whitespace-only description strings
to ensure meta is omitted or contains only id per existing behavior. Use the
existing helper functions (generateZodValidationSchemaDefinition,
parseZodValidationSchemaDefinition) and the def.functions lookups for 'meta' and
'describe' to validate outputs.

7961-7963: 💤 Low value

Exact string matching may be brittle for object literals.

The assertion checks for an exact string match including key order and formatting:

".meta({ id: 'Pet', description: 'A pet', deprecated: true })"

While acceptable for unit tests, consider whether key order is guaranteed. If the object serialization changes (e.g., keys reordered, quote style changes), this test would break. For more robust testing, you could:

  • Parse the output and compare structure
  • Use regex patterns for flexible matching
  • Split into multiple .toContain() checks

However, exact string matching provides strong guarantees about output format, so the current approach is reasonable if the format is intentional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/zod/src/zod.test.ts` around lines 7961 - 7963, The assertion using
parsed.zod checks for an exact object-literal string which is brittle to key
order/formatting; update the test around parsed.zod to perform a more robust
check—either replace the exact .toContain(...) string with a regex that matches
the .meta(...) call and the required keys/values (e.g., ensure substrings like
"id: 'Pet'", "description: 'A pet'", and "deprecated: true" appear) or split
into multiple .toContain() assertions verifying each key/value and the ".meta("
wrapper, or parse the generated output and compare the object structure
programmatically; target the assertion using the parsed.zod reference when
making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/zod/src/index.ts`:
- Around line 223-230: The early-return fast path that returns a plain `$ref`
schema when encountering chainable `$ref` siblings skips adding the top-level
metadata; update the branch that handles reusable `$ref` aliases to check
`rules.emitMeta` (or `emitMeta`) and, when true, call `.meta({ id, description?,
deprecated? })` on the returned Zod schema before returning it so the registry
`id` is present; locate the early-return for chainable `$ref` siblings (the
logic that currently returns the `$ref` directly) and add a conditional wrapping
step that attaches `.meta(...)` using the same `id` generation used elsewhere
(matching how top-level component code adds metadata) so the behavior is
consistent with the non-fast-path.

---

Nitpick comments:
In `@packages/orval/src/write-zod-specs.test.ts`:
- Around line 625-639: The test currently asserts an exact substring match for
the generated .meta object which is brittle to field order/formatting; update
the assertion in the test case that uses generateZodSchemasInline(metaBuilder(),
output) so it instead checks for the presence of the individual metadata
properties (e.g., that the result contains "id: 'Pet'", "description: 'A pet'",
and "deprecated: true") or use a regex that tolerates any key order/spacing;
locate the test block referencing createOutputOptions(),
zodOverride.generateMeta, metaBuilder(), and generateZodSchemasInline and
replace the strict expect(result).toContain(".meta({...})") with separate
contains assertions or an order-agnostic regex check.

In `@packages/zod/src/zod.test.ts`:
- Around line 7919-8063: Add additional unit tests in
packages/zod/src/zod.test.ts that exercise partial metadata and edge cases:
create cases where only description exists (no deprecated) and only deprecated
exists to assert def.functions contains the expected meta payload via
generateZodValidationSchemaDefinition and parseZodValidationSchemaDefinition;
add tests that ensure .meta() appears after modifiers by building schemas with
optional/nullable/default and asserting parsed.zod shows modifiers before
".meta(...)" (use the same def.functions and parsed.zod checks as existing
tests); add top-level oneOf/allOf/anyOf schemas to confirm meta is emitted for
those constructs; and add tests for empty or whitespace-only description strings
to ensure meta is omitted or contains only id per existing behavior. Use the
existing helper functions (generateZodValidationSchemaDefinition,
parseZodValidationSchemaDefinition) and the def.functions lookups for 'meta' and
'describe' to validate outputs.
- Around line 7961-7963: The assertion using parsed.zod checks for an exact
object-literal string which is brittle to key order/formatting; update the test
around parsed.zod to perform a more robust check—either replace the exact
.toContain(...) string with a regex that matches the .meta(...) call and the
required keys/values (e.g., ensure substrings like "id: 'Pet'", "description: 'A
pet'", and "deprecated: true" appear) or split into multiple .toContain()
assertions verifying each key/value and the ".meta(" wrapper, or parse the
generated output and compare the object structure programmatically; target the
assertion using the parsed.zod reference when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0cd1688c-a6fd-460c-9107-e8a9a45b36c2

📥 Commits

Reviewing files that changed from the base of the PR and between 14d5a0f and 49d89c3.

📒 Files selected for processing (9)
  • docs/content/docs/reference/configuration/output.mdx
  • packages/core/src/test-utils/context.ts
  • packages/core/src/types.ts
  • packages/orval/src/reusable-schemas.ts
  • packages/orval/src/utils/options.ts
  • packages/orval/src/write-zod-specs.test.ts
  • packages/orval/src/write-zod-specs.ts
  • packages/zod/src/index.ts
  • packages/zod/src/zod.test.ts

Comment thread packages/zod/src/index.ts
@melloware

Copy link
Copy Markdown
Collaborator

@z4o4z conflicts

@melloware melloware left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolve conflicts

z4o4z and others added 2 commits May 28, 2026 14:15
…od v4)

New opt-in `override.zod.generateMeta` (default false). When enabled on zod v4,
each generated component schema gets registry metadata via `.meta({ id,
description?, deprecated? })`: `id` is the schema name, with `description` /
`deprecated` included when the OpenAPI schema provides them. This makes the
schemas self-describing and lets `z.toJSONSchema()` reference them by `id`.

- Generator: a top-level-only `emitMeta` rule folds the schema's trailing
  `.describe(...)` into a single `.meta({...})` on zod v4; nested properties keep
  `.describe()`. `.meta()` is emitted last (zod v4 turns `.meta().describe()`
  into a `$ref` wrapper, whereas `.describe().meta()` stays flat). Handled at
  both generator exits (multi-type union + main).
- Scope: component schemas only (named exports). Operation wrappers are left
  untouched so zod's global-registry `id`s stay unique.
- zod v3 has no `.meta()` — the option is a no-op and descriptions still emit via
  `.describe()`.

Verified: generated v4 output type-checks under strict and round-trips through
`z.toJSONSchema` (id/description/deprecated surface in the JSON Schema).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rage

- Restore prior empty-string description semantics: empty `''` (or non-string)
  descriptions are treated as absent in `pushDescriptionOrMeta`, so neither
  `.describe('')` nor `description: ''` is emitted. Matches the old
  `if (schema.description)` falsy check (Copilot review).
- Add `generateMeta: false` to the inline `NormalizedZodOptions` test fixtures
  in `@orval/mock`, `@orval/angular` (×2), and `@orval/solid-start` — the
  monorepo typecheck was failing there because `generateMeta` is required on
  the normalized type.
- Tests: regression for the empty-string fix (v3, v4, and emitMeta-off); pin
  the use-site arrangement when a `$ref` has a `description` sibling
  (`Pet.describe(...)` chained AFTER Pet's `.meta(...)`), which is the
  documented zod v4 pattern that round-trips through `z.toJSONSchema` as
  `{ description, $ref: '#/$defs/Pet' }`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Actionable comments posted: 0

@z4o4z
z4o4z requested a review from melloware May 28, 2026 13:58
@melloware
melloware merged commit f245ea6 into orval-labs:master May 28, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

zod Zod schema client related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants