Skip to content

feat(v27): Zod Plugin v4: Moving brand to inheritable metadata#3162

Merged
RobinTail merged 13 commits intomake-v27from
util-inheritable-meta43
Jan 2, 2026
Merged

feat(v27): Zod Plugin v4: Moving brand to inheritable metadata#3162
RobinTail merged 13 commits intomake-v27from
util-inheritable-meta43

Conversation

@RobinTail
Copy link
Copy Markdown
Owner

@RobinTail RobinTail commented Jan 2, 2026

Thanks to #3156 storing the runtime distinguishable brand in the bag is no longer required, since metadata became inheritable between zod schemas in globalRegistry starting from Zod v4.3.0.

See https://github.com/colinhacks/zod/pull/5578/changes for implementation.

Breaking changes to the Zod Plugin:

  • min Zod version 4.3.4
  • pack() and unpack() removed (no longer needed)

Summary by CodeRabbit

  • New Features

    • Metadata now inherits across chained schema operations and constraints.
  • Breaking Changes

    • Removed pack/unpack utilities; use schema.meta() for metadata.
    • Brand metadata moved to a namespaced meta key ("x-brand"); retrieve via schema.meta() or the getBrand() helper.
  • Chores

    • Bumped Zod requirement to ^4.3.4 and plugin version to 4.0.0-beta.1.

✏️ Tip: You can customize this high-level summary in your review settings.

@RobinTail RobinTail added this to the v27 milestone Jan 2, 2026
@RobinTail RobinTail added the coverage Additional tests label Jan 2, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 2, 2026

📝 Walkthrough

Walkthrough

Bumps zod requirement to ^4.3.4, removes the custom pack/unpack mechanism, and migrates schema branding to use Zod metadata and globalRegistry (brand stored as x-brand via schema.meta()); tests and docs updated and package version bumped to 4.0.0-beta.1.

Changes

Cohort / File(s) Summary
Dependency & Workspace
pnpm-workspace.yaml
Zod peer/dev requirement updated to ^4.3.4.
Package Manifest
zod-plugin/package.json
Version bumped 3.0.04.0.0-beta.1.
Docs / Changelog / README
zod-plugin/CHANGELOG.md, zod-plugin/README.md
Documented v4 changes: Zod 4.3.4 support, removal of pack/unpack, migration to schema.meta() and x-brand; README examples updated.
Brand implementation
zod-plugin/src/brand.ts, zod-plugin/src/meta.ts, zod-plugin/src/runtime.ts
Replaced pack/unpack/setBrand usage: brandProperty renamed to x-brand; getBrand reads from globalRegistry; added brandSetter that calls this.meta({ [brandProperty]: ... }); runtime now uses brandSetter.
Removed packer
zod-plugin/src/packer.ts (deleted)
Entire pack/unpack implementation and related internals removed.
Public exports
zod-plugin/src/index.ts
Removed re-exports of pack/unpack; getBrand retained.
Tests added/updated
express-zod-api/tests/env.spec.ts, express-zod-api/tests/integration.spec.ts, zod-plugin/tests/*
Added metadata inheritance test; updated integration and plugin tests to use globalRegistry/schema.meta() and x-brand; removed packer tests and pack/unpack assertions.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • #2869 — touches zod-plugin exports and branding/packer surface similar to this refactor.
  • #3087 — modifies runtime/meta handling for zod-plugin; likely overlaps with brand/meta changes.
  • #2877 — changes schema brand implementation and runtime behavior related to this PR.

Suggested labels

dependencies, documentation, refactoring

Poem

🐰 I hopped through code and left a mark,

x-brand tucked softly in schema's park,
No more packing in shadowed sacks,
Meta now holds what once was wrapped,
A tiny hop toward cleaner tracks.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title accurately and specifically describes the main change: migrating the brand functionality to use Zod's inheritable metadata feature introduced in v4.3.
✨ Finishing touches
  • 📝 Generate docstrings

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.

@coveralls-official
Copy link
Copy Markdown

coveralls-official Bot commented Jan 2, 2026

Coverage Status

coverage: 100.0%. remained the same
when pulling 62e4138 on util-inheritable-meta43
into 611967d on make-v27.

@RobinTail RobinTail added the breaking Backward incompatible changes label Jan 2, 2026
@RobinTail RobinTail added the enhancement New feature or request label Jan 2, 2026
@RobinTail RobinTail marked this pull request as ready for review January 2, 2026 09:05
@RobinTail
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Comment thread zod-plugin/src/brand.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 611967d and a971520.

📒 Files selected for processing (12)
  • express-zod-api/tests/env.spec.ts
  • express-zod-api/tests/integration.spec.ts
  • pnpm-workspace.yaml
  • zod-plugin/CHANGELOG.md
  • zod-plugin/README.md
  • zod-plugin/package.json
  • zod-plugin/src/brand.ts
  • zod-plugin/src/index.ts
  • zod-plugin/src/packer.ts
  • zod-plugin/tests/brand.spec.ts
  • zod-plugin/tests/index.spec.ts
  • zod-plugin/tests/packer.spec.ts
💤 Files with no reviewable changes (4)
  • zod-plugin/src/index.ts
  • zod-plugin/src/packer.ts
  • zod-plugin/tests/packer.spec.ts
  • zod-plugin/tests/index.spec.ts
🧰 Additional context used
🧠 Learnings (26)
📓 Common learnings
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 0
File: :0-0
Timestamp: 2025-08-08T16:45:20.527Z
Learning: express-zod-api/zod-plugin: The ZodType.prototype.brand getter returns setBrand.bind(this) each time (no memoization). setBrand must always call pack() and include { brand: undefined } when unbranding so that pack → .check() runs and prior brand is overwritten; do not suggest removing the explicit undefined entry.
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 0
File: :0-0
Timestamp: 2025-08-08T16:45:20.527Z
Learning: In express-zod-api zod-plugin, the brand setter must be immutable and always call `.check()` (packer’s onattach). Therefore, writing an explicit undefined entry in the bag for brand is intentional and should not be suggested for removal.
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 0
File: :0-0
Timestamp: 2025-08-08T16:45:20.527Z
Learning: express-zod-api/zod-plugin: The brand setter must be immutable and always invoke pack→.check() via the getter-bound setBrand. Including brandProperty with an explicit undefined enables unbranding and should not be optimized away.
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 0
File: :0-0
Timestamp: 2025-08-08T16:45:20.527Z
Learning: express-zod-api zod-plugin: The brand setter must stay immutable and always invoke pack→.check() (onattach). Including brandProperty with an explicit undefined enables “unbranding” (overwriting a previous brand) and should not be optimized away.
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2878
File: zod-plugin/runtime.ts:39-42
Timestamp: 2025-08-08T11:59:04.814Z
Learning: express-zod-api/zod-plugin/runtime.ts (TypeScript): Do not memoize the ZodType.prototype.brand getter. The current design returning setBrand.bind(this) on each access is intentional/preferred; avoid redefining "brand" as a data property per instance.
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2697
File: CHANGELOG.md:5-5
Timestamp: 2025-06-02T21:11:20.768Z
Learning: In the express-zod-api repository, RobinTail follows a release workflow where package.json version is only updated on the master branch after merging all planned release changes. Changelog entries may show future version numbers while package.json remains at the previous version during feature development, and this is intentional workflow, not a version inconsistency that needs to be flagged.
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2546
File: express-zod-api/tests/form-schema.spec.ts:31-31
Timestamp: 2025-05-27T19:27:13.492Z
Learning: Zod version 3.25.0 and later expose the Zod v4 API through the special import paths "zod/v4" and "zod/v4/core", allowing v4 features like .loose() to be used even when the package.json dependency shows a 3.x version.
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2869
File: zod-plugin/package.json:42-44
Timestamp: 2025-08-05T14:43:24.702Z
Learning: Zod version 4 has been officially released and is stable. As of August 2025, the latest published version is 4.0.14 (published July 30, 2025). Zod v4 is available on npm and can be used in peer dependencies with version constraints like "^4.0.0".
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2546
File: express-zod-api/src/metadata.ts:5-13
Timestamp: 2025-05-27T20:08:50.699Z
Learning: The `_zod` property in Zod v4 schemas is officially documented and recommended for library authors to differentiate between Zod 3 and Zod 4 schemas at runtime, despite the underscore prefix. This is explicitly mentioned in Zod's library authors documentation and is not a private internal property.
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 0
File: :0-0
Timestamp: 2025-08-01T09:48:13.742Z
Learning: In express-zod-api, when migrating from Zod v3 to v4, the correct approach for internal type imports is to change from `import type { $ZodType } from "zod/v4/core"` to `import { z } from "zod"` and then use `z.core.$ZodType`. The zod/v4/core module is reexported as z.core by the main zod package, making this a valid and working approach.
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2546
File: express-zod-api/src/json-schema-helpers.ts:1-3
Timestamp: 2025-05-27T20:27:17.015Z
Learning: The JSONSchema type is not exported from the main "zod" module and must be imported from "zod/v4/core" when using Zod v4. This is acceptable for type-only imports as they don't create runtime dependencies.
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2546
File: express-zod-api/src/json-schema-helpers.ts:1-3
Timestamp: 2025-05-27T20:27:17.015Z
Learning: The JSONSchema type is not exported from the main "zod" module and must be imported from "zod/v4/core" when using Zod v4. This is acceptable for type-only imports as they don't create runtime dependencies.
📚 Learning: 2025-08-05T14:43:24.702Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2869
File: zod-plugin/package.json:42-44
Timestamp: 2025-08-05T14:43:24.702Z
Learning: Zod version 4 has been officially released and is stable. As of August 2025, the latest published version is 4.0.14 (published July 30, 2025). Zod v4 is available on npm and can be used in peer dependencies with version constraints like "^4.0.0".

Applied to files:

  • zod-plugin/package.json
  • zod-plugin/README.md
  • pnpm-workspace.yaml
  • zod-plugin/CHANGELOG.md
📚 Learning: 2025-05-27T19:27:13.492Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2546
File: express-zod-api/tests/form-schema.spec.ts:31-31
Timestamp: 2025-05-27T19:27:13.492Z
Learning: Zod version 3.25.0 and later expose the Zod v4 API through the special import paths "zod/v4" and "zod/v4/core", allowing v4 features like .loose() to be used even when the package.json dependency shows a 3.x version.

Applied to files:

  • zod-plugin/package.json
  • zod-plugin/README.md
  • pnpm-workspace.yaml
  • express-zod-api/tests/integration.spec.ts
  • zod-plugin/CHANGELOG.md
  • zod-plugin/src/brand.ts
📚 Learning: 2025-06-02T21:11:20.768Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2697
File: CHANGELOG.md:5-5
Timestamp: 2025-06-02T21:11:20.768Z
Learning: In the express-zod-api repository, RobinTail follows a release workflow where package.json version is only updated on the master branch after merging all planned release changes. Changelog entries may show future version numbers while package.json remains at the previous version during feature development, and this is intentional workflow, not a version inconsistency that needs to be flagged.

Applied to files:

  • zod-plugin/package.json
  • pnpm-workspace.yaml
📚 Learning: 2025-08-01T09:48:13.742Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 0
File: :0-0
Timestamp: 2025-08-01T09:48:13.742Z
Learning: In express-zod-api, when migrating from Zod v3 to v4, the correct approach for internal type imports is to change from `import type { $ZodType } from "zod/v4/core"` to `import { z } from "zod"` and then use `z.core.$ZodType`. The zod/v4/core module is reexported as z.core by the main zod package, making this a valid and working approach.

Applied to files:

  • zod-plugin/package.json
  • zod-plugin/README.md
  • pnpm-workspace.yaml
  • express-zod-api/tests/integration.spec.ts
  • zod-plugin/tests/brand.spec.ts
  • zod-plugin/CHANGELOG.md
  • express-zod-api/tests/env.spec.ts
  • zod-plugin/src/brand.ts
📚 Learning: 2025-05-27T20:08:50.699Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2546
File: express-zod-api/src/metadata.ts:5-13
Timestamp: 2025-05-27T20:08:50.699Z
Learning: The `_zod` property in Zod v4 schemas is officially documented and recommended for library authors to differentiate between Zod 3 and Zod 4 schemas at runtime, despite the underscore prefix. This is explicitly mentioned in Zod's library authors documentation and is not a private internal property.

Applied to files:

  • zod-plugin/package.json
  • zod-plugin/README.md
  • express-zod-api/tests/integration.spec.ts
  • zod-plugin/CHANGELOG.md
  • zod-plugin/src/brand.ts
📚 Learning: 2025-09-29T06:00:16.488Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2974
File: express-zod-api/src/documentation-helpers.ts:48-48
Timestamp: 2025-09-29T06:00:16.488Z
Learning: In the express-zod-api repository, the project uses pnpm workspaces with autoInstallPeers: false, meaning peerDependencies are not installed. The actual TypeScript version used for development comes from devDependencies which references "catalog:dev" in pnpm-workspace.yaml where it's set to a recent version (^5.9.2), not the peerDependency version which is just the minimum consumer requirement.

Applied to files:

  • zod-plugin/package.json
  • pnpm-workspace.yaml
  • express-zod-api/tests/integration.spec.ts
📚 Learning: 2025-08-08T16:45:20.527Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 0
File: :0-0
Timestamp: 2025-08-08T16:45:20.527Z
Learning: express-zod-api/zod-plugin: The brand setter must be immutable and always invoke pack→.check() via the getter-bound setBrand. Including brandProperty with an explicit undefined enables unbranding and should not be optimized away.

Applied to files:

  • zod-plugin/package.json
  • zod-plugin/README.md
  • express-zod-api/tests/integration.spec.ts
  • zod-plugin/tests/brand.spec.ts
  • zod-plugin/src/brand.ts
📚 Learning: 2025-09-29T03:35:55.561Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 0
File: :0-0
Timestamp: 2025-09-29T03:35:55.561Z
Learning: In the express-zod-api repository, packages are built using tsdown before publishing. Source code with .ts extensions is not published - only the built JavaScript bundles and declaration files in the dist/ directory are published to npm. This means .ts extensions in source imports don't affect consumers.

Applied to files:

  • zod-plugin/package.json
  • pnpm-workspace.yaml
  • express-zod-api/tests/integration.spec.ts
📚 Learning: 2025-05-27T20:03:34.213Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2546
File: example/factories.ts:35-42
Timestamp: 2025-05-27T20:03:34.213Z
Learning: The `./example` directory in the express-zod-api repository contains demonstration code for educational purposes only, not intended for production use. Example code can make simplified assumptions for brevity and clarity, and should not be flagged for missing production-level error handling, security measures, or edge case handling.

Applied to files:

  • zod-plugin/package.json
📚 Learning: 2025-10-02T17:42:48.840Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 0
File: :0-0
Timestamp: 2025-10-02T17:42:48.840Z
Learning: In express-zod-api v25 (ESM-only), the `.example()` method error occurs when user code runs as CommonJS. Express Zod API patches Zod's ESM bundle with `.example()`, but CommonJS code requires a separate CJS bundle instance that lacks this patch. Users must run their code as ESM by: (1) setting `"type": "module"` in package.json, (2) using `.mts` or `.mjs` file extensions, or (3) using tools like `tsx` or `vite-node` that provide their own ESM-compatible compilation.

Applied to files:

  • zod-plugin/package.json
  • express-zod-api/tests/integration.spec.ts
  • zod-plugin/tests/brand.spec.ts
📚 Learning: 2025-05-27T20:27:17.015Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2546
File: express-zod-api/src/json-schema-helpers.ts:1-3
Timestamp: 2025-05-27T20:27:17.015Z
Learning: Ramda is correctly listed as a dependency in express-zod-api/package.json, so imports of ramda utilities are properly supported.

Applied to files:

  • zod-plugin/package.json
  • pnpm-workspace.yaml
📚 Learning: 2025-08-08T16:45:20.527Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 0
File: :0-0
Timestamp: 2025-08-08T16:45:20.527Z
Learning: express-zod-api/zod-plugin: The ZodType.prototype.brand getter returns setBrand.bind(this) each time (no memoization). setBrand must always call pack() and include { brand: undefined } when unbranding so that pack → .check() runs and prior brand is overwritten; do not suggest removing the explicit undefined entry.

Applied to files:

  • zod-plugin/README.md
  • express-zod-api/tests/integration.spec.ts
  • zod-plugin/tests/brand.spec.ts
  • zod-plugin/CHANGELOG.md
  • zod-plugin/src/brand.ts
📚 Learning: 2025-08-08T11:59:04.814Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2878
File: zod-plugin/runtime.ts:39-42
Timestamp: 2025-08-08T11:59:04.814Z
Learning: express-zod-api/zod-plugin/runtime.ts (TypeScript): Do not memoize the ZodType.prototype.brand getter. The current design returning setBrand.bind(this) on each access is intentional/preferred; avoid redefining "brand" as a data property per instance.

Applied to files:

  • zod-plugin/README.md
  • express-zod-api/tests/integration.spec.ts
  • zod-plugin/tests/brand.spec.ts
  • zod-plugin/src/brand.ts
📚 Learning: 2025-08-08T16:45:20.527Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 0
File: :0-0
Timestamp: 2025-08-08T16:45:20.527Z
Learning: express-zod-api zod-plugin: The brand setter must stay immutable and always invoke pack→.check() (onattach). Including brandProperty with an explicit undefined enables “unbranding” (overwriting a previous brand) and should not be optimized away.

Applied to files:

  • zod-plugin/README.md
  • express-zod-api/tests/integration.spec.ts
  • zod-plugin/tests/brand.spec.ts
  • zod-plugin/CHANGELOG.md
  • zod-plugin/src/brand.ts
📚 Learning: 2025-08-08T16:45:20.527Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 0
File: :0-0
Timestamp: 2025-08-08T16:45:20.527Z
Learning: In express-zod-api zod-plugin, the brand setter must be immutable and always call `.check()` (packer’s onattach). Therefore, writing an explicit undefined entry in the bag for brand is intentional and should not be suggested for removal.

Applied to files:

  • zod-plugin/README.md
  • express-zod-api/tests/integration.spec.ts
  • zod-plugin/tests/brand.spec.ts
  • zod-plugin/CHANGELOG.md
  • zod-plugin/src/brand.ts
📚 Learning: 2025-05-28T07:58:09.853Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2546
File: express-zod-api/src/documentation-helpers.ts:508-512
Timestamp: 2025-05-28T07:58:09.853Z
Learning: In express-zod-api, when working with Zod's JSON schema override callbacks, using `delete` to mutate `ctx.jsonSchema` is the recommended approach per Zod's official documentation, even if it triggers performance linting warnings. This is preferable to creating copies with `undefined` values, especially for snapshot testing.

Applied to files:

  • zod-plugin/README.md
  • express-zod-api/tests/integration.spec.ts
📚 Learning: 2025-05-28T19:19:27.727Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 0
File: :0-0
Timestamp: 2025-05-28T19:19:27.727Z
Learning: In pnpm workspaces, most configuration can now be done directly in pnpm-workspace.yaml using the `settings` section instead of requiring .npmrc files. This includes workspace linking, dependency resolution, and performance settings.

Applied to files:

  • pnpm-workspace.yaml
📚 Learning: 2025-12-18T13:42:39.225Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 3054
File: pnpm-workspace.yaml:68-68
Timestamp: 2025-12-18T13:42:39.225Z
Learning: In the express-zod-api repository, Vite is overridden to v8 beta in pnpm-workspace.yaml specifically to ensure vitest (which depends on Vite) uses the same rolldown bundler as tsdown and unrun. This override is intentional for toolchain consistency, as Vite is only used for testing purposes (not production).

Applied to files:

  • pnpm-workspace.yaml
📚 Learning: 2025-06-02T21:08:56.475Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2697
File: CHANGELOG.md:5-5
Timestamp: 2025-06-02T21:08:56.475Z
Learning: The `cjs-test` directory in the express-zod-api repository is a test workspace and should be excluded when checking for main project version consistency with changelog entries.

Applied to files:

  • pnpm-workspace.yaml
  • express-zod-api/tests/integration.spec.ts
  • express-zod-api/tests/env.spec.ts
📚 Learning: 2025-05-27T20:27:17.015Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2546
File: express-zod-api/src/json-schema-helpers.ts:1-3
Timestamp: 2025-05-27T20:27:17.015Z
Learning: The JSONSchema type is not exported from the main "zod" module and must be imported from "zod/v4/core" when using Zod v4. This is acceptable for type-only imports as they don't create runtime dependencies.

Applied to files:

  • pnpm-workspace.yaml
  • express-zod-api/tests/integration.spec.ts
  • zod-plugin/tests/brand.spec.ts
  • zod-plugin/CHANGELOG.md
  • zod-plugin/src/brand.ts
📚 Learning: 2025-05-27T19:35:57.357Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2546
File: express-zod-api/tests/buffer-schema.spec.ts:32-37
Timestamp: 2025-05-27T19:35:57.357Z
Learning: In the express-zod-api project, tests are run from the `express-zod-api` workspace directory, and the project uses an ESM-first environment without `__dirname`. Relative paths like `../logo.svg` in test files correctly resolve to the repository root due to this test execution context.

Applied to files:

  • express-zod-api/tests/integration.spec.ts
  • zod-plugin/tests/brand.spec.ts
  • express-zod-api/tests/env.spec.ts
📚 Learning: 2025-06-14T16:42:52.972Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2736
File: express-zod-api/tsup.config.ts:12-26
Timestamp: 2025-06-14T16:42:52.972Z
Learning: In express-zod-api tsup configurations, the direct mutation of `options.supported` in the `esbuildOptions` callback is intentional behavior and should not be flagged as a side effect issue.

Applied to files:

  • express-zod-api/tests/integration.spec.ts
  • express-zod-api/tests/env.spec.ts
📚 Learning: 2025-05-28T18:58:10.064Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2428
File: express-zod-api/src/index.ts:44-44
Timestamp: 2025-05-28T18:58:10.064Z
Learning: The type-only import `import type {} from "qs";` in express-zod-api/src/index.ts is necessary to avoid TS2742 errors for exported functions like attachRouting, makeRequestMock, testEndpoint, and testMiddleware that have types depending on types/qs. This import provides the reference TypeScript needs to infer portable type names.

Applied to files:

  • express-zod-api/tests/integration.spec.ts
📚 Learning: 2025-05-27T20:22:30.428Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2546
File: express-zod-api/tests/zts.spec.ts:160-162
Timestamp: 2025-05-27T20:22:30.428Z
Learning: In express-zod-api/tests/zts.spec.ts, the `Fruits` enum intentionally contains both string and numeric members (Apple = "apple", Banana = "banana", Cantaloupe = "cantaloupe", A = 5) and is used with `z.enum(Fruits)` to test how the system handles mixed enum types. This is by design for testing purposes.

Applied to files:

  • zod-plugin/tests/brand.spec.ts
  • express-zod-api/tests/env.spec.ts
📚 Learning: 2025-09-08T09:24:05.821Z
Learnt from: RobinTail
Repo: RobinTail/express-zod-api PR: 2922
File: example/endpoints/list-users.ts:4-4
Timestamp: 2025-09-08T09:24:05.821Z
Learning: In Zod v4, z.literal() accepts arrays and supports multiple values in a single schema (e.g., z.literal(["manager", "operator", "admin"]) is valid), unlike previous versions where it only accepted single literal values. The .value property was replaced with .values which is a Set.

Applied to files:

  • zod-plugin/CHANGELOG.md
🧬 Code graph analysis (2)
express-zod-api/tests/integration.spec.ts (1)
express-zod-api/src/middleware.ts (1)
  • schema (82-84)
zod-plugin/tests/brand.spec.ts (2)
zod-plugin/src/brand.ts (3)
  • brandProperty (4-4)
  • setBrand (7-9)
  • getBrand (15-24)
zod-plugin/src/index.ts (1)
  • getBrand (2-2)
🔇 Additional comments (13)
pnpm-workspace.yaml (1)

53-53: LGTM!

The Zod version bump to ^4.3.4 is consistent across both peer and dev catalogs, properly aligning with the PR's goal of utilizing inheritable metadata introduced in Zod 4.3.0.

Also applies to: 68-68

zod-plugin/package.json (1)

3-3: LGTM!

The major version bump to 4.0.0-beta.1 is appropriate given the breaking changes (removal of pack() and unpack() APIs). The beta designation allows for user feedback before stable release.

express-zod-api/tests/env.spec.ts (1)

132-136: LGTM!

This test correctly validates Zod 4.3's metadata inheritance feature, which is the foundation for this PR's changes. The test demonstrates that metadata from a parent schema propagates through schema transformations like .min(1).

zod-plugin/CHANGELOG.md (1)

3-10: LGTM!

The changelog entry accurately documents the breaking changes:

  • Zod version requirement bump to ^4.3.4
  • Removal of pack() and unpack() (with clear migration path to .meta())
  • Brand metadata storage change
zod-plugin/tests/brand.spec.ts (3)

1-1: LGTM!

Import correctly adds globalRegistry from zod to support the new metadata retrieval mechanism.


12-16: LGTM!

The test correctly verifies that setBrand stores the brand under the "x-brand" key via the schema's .meta() method.


20-27: LGTM!

The parameterized test comprehensively covers all edge cases for getBrand:

  1. Metadata with brand present → returns the brand value
  2. Empty metadata object → returns undefined
  3. No metadata registered → returns undefined

The conditional if (metadata) correctly skips registry manipulation for the undefined case.

express-zod-api/tests/integration.spec.ts (2)

2-2: LGTM!

Import correctly adds globalRegistry from zod to support the registry-based brand clearing mechanism.


136-136: Verify: globalRegistry.remove() clears all metadata, not just the brand.

The previous implementation (schema._zod.bag.brand = undefined) only cleared the brand property. The new approach using globalRegistry.remove(schema) removes the entire schema from the registry, clearing all metadata (examples, descriptions, etc.), not just the brand.

If this behavioral difference is intentional for this test case, this is fine. However, if the intent is to only clear the brand while preserving other metadata, consider using a more targeted approach.

zod-plugin/src/brand.ts (4)

3-4: LGTM! OpenAPI-compliant naming.

The rename to "x-brand" correctly follows the OpenAPI convention of prefixing vendor extensions with "x-".


7-9: The implementation is correct. The test at line 12-16 of brand.spec.ts confirms that .meta() preserves immutability: const subject = setBrand.call(parent, "test") returns a new schema instance with the metadata properly set, verified by expect(subject.meta()).toHaveProperty(brandProperty, "test"). The migration from pack() to Zod's native .meta() API is sound and working as expected.


1-1: The globalRegistry API is officially available and stable in Zod 4.3.4. It is a documented global registry for schema metadata with core methods including get(), has(), remove(), and clear(), plus convenience methods like .meta(). The import from the main "zod" package is correct, and the API is part of Zod's official interface.


15-24: Remove the verification request—the implementation is correct.

The getBrand function properly retrieves brand metadata from globalRegistry with a safe fallback and type guards. The codebase pattern intentionally applies .brand() at the terminal point of schema chains, after transformations (see date-in-schema.ts and date-out-schema.ts), rather than expecting brands to be inherited through transforms. Tests confirm brand metadata is preserved through refinements and schema description, which validates the design.

Comment thread zod-plugin/README.md Outdated
@RobinTail
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@RobinTail RobinTail changed the title feat(v27): Utilize inheritable metadata (zod 4.3) feat(v27): Zod Plugin v4: Moving brand to inheritable metadata Jan 2, 2026
@RobinTail RobinTail merged commit 9506dbc into make-v27 Jan 2, 2026
13 checks passed
@RobinTail RobinTail deleted the util-inheritable-meta43 branch January 2, 2026 09:24
RobinTail added a commit that referenced this pull request Feb 1, 2026
<img width="1200" height="630" alt="image"
src="https://github.com/user-attachments/assets/5af2b247-bd39-4481-98a4-0ab6586afe49"
/>

Nikki Kuhnhausen was 17 years young transgender woman. She was murdered
by David Bogdanov in 2019. Her body was identified from decayed remains
on December 7th, in the Larch Mountain area near Vancouver, Washington.
Authorities were able to identify the body from personal effects;
subsequent examination showed she died from strangulation. Nikki had
previously been missing since June 5th when she left with David
Bogdanov, a 25-year-old male she met in Snapchat, who has since been
charged with second degree murder. Authorities believe that Bogdanov,
looking for a sexual encounter, became enraged upon learning of Nikki's
transgender origins.


https://katu.com/news/local/missing-vancouver-teen-nikki-kuhnhausen-found-dead-in-remote-clark-county-location-suspect-arrested

Transgender women suffer too frequently from transphobic violence and
cruelty, being the less protected social group. I'd like to raise an
awareness of this problem. Humans should be creators — not killers. But
most importantly, I want every transgender girl to have an opportunity
to create applications quickly and, in general, learn to write code
easily in order to receive job offers and leave dangerously transphobic
territories for more favorable and civilized ones, and live happily
there. Protect transgender women.

------------------

Version 27 makes `typescript` an optional peer dependency with explicit
injection of the manually imported module into the `Integration` class
constructor (`new Integration({ typescript, ... })`), or by calling the
new async `Integration.create()` factory method for automatic loading.
The Zod Plugin now leverages Zod 4.3's inheritable metadata feature,
removing the need for `pack()` and `unpack()` helpers while simplifying
brand handling by storing brands in standard metadata under `x-brand`
property.

- #3149 
- #3162 

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Async Integration.create() factory and optional TypeScript injection
for client generation
  * Zod metadata is inheritable (requires Zod ^4.3.4)

* **Breaking Changes**
* pack/unpack/setBrand removed; branding exposed as "x-brand" via
schema.meta()
* Integration constructor now accepts a typescript option and usage may
require async create()

* **Documentation**
  * Examples updated for TypeScript injection and async create() usage

* **Chores**
  * Version bumps to v27 beta and updated CI branch targets

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Backward incompatible changes coverage Additional tests enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant