Skip to content

feat(zod-plugin): Abstract methods pack() and unpack()#2878

Merged
RobinTail merged 23 commits intomasterfrom
plugin-abstract-bag
Aug 9, 2025
Merged

feat(zod-plugin): Abstract methods pack() and unpack()#2878
RobinTail merged 23 commits intomasterfrom
plugin-abstract-bag

Conversation

@RobinTail
Copy link
Copy Markdown
Owner

@RobinTail RobinTail commented Aug 8, 2025

This should simplify creating more advanced branded schemas, such as function one.

Summary by CodeRabbit

  • New Features

    • Introduced utilities to associate and retrieve brand identifiers on Zod schemas at runtime.
    • Added the ability to attach arbitrary metadata ("bags") to Zod schemas.
    • Added a remapping utility for Zod object schemas, supporting both mapping objects and functions.
  • Refactor

    • Replaced internal branding and mapping logic with modular, reusable utilities.
    • Updated schema branding and remapping methods to use new implementations.
  • Bug Fixes

    • Improved handling of metadata persistence across schema refinements.
  • Tests

    • Added comprehensive tests for branding, metadata packing/unpacking, and remapping utilities.
    • Removed outdated tests for deprecated helpers.
    • Updated export verification tests to include new utility functions.
  • Chores

    • Cleaned up unused files and internal helper functions.
  • Documentation

    • Enhanced documentation to clarify branding behavior and introduced helper utilities.

@RobinTail RobinTail added enhancement New feature or request refactoring The better way to achieve the same result labels Aug 8, 2025
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Aug 8, 2025

Walkthrough

This change removes the custom $EZBrandCheck mechanism and the getBrand helper, replacing them with a new metadata packing system (packer.ts) that allows arbitrary metadata ("bags") to be associated with Zod schemas. The branding logic is refactored into a new brand.ts module, and the remap utility is reimplemented for Zod objects. Related tests are updated, removed, or added accordingly.

Changes

Cohort / File(s) Change Summary
Remove Custom Brand Check & Helper
zod-plugin/brand-check.ts, zod-plugin/helpers.ts
Deleted the custom $EZBrandCheck implementation and the getBrand helper function.
Introduce Brand & Packer Utilities
zod-plugin/brand.ts, zod-plugin/packer.ts
Added new modules for branding (brand.ts) and metadata packing/unpacking (packer.ts) for Zod schemas.
Implement Remap Utility
zod-plugin/remap.ts
Added a new remap method for Zod objects, supporting runtime property remapping with cloning and transformation.
Refactor Runtime Augmentations
zod-plugin/runtime.ts
Replaced internal brand/remap logic with imports from the new brand and remap modules; removed previous internal implementations.
Update Index Exports
zod-plugin/index.ts
Changed exports: now exports getBrand from brand.ts, and adds pack/unpack from packer.ts.
Adjust and Add Tests
zod-plugin/brand.spec.ts, zod-plugin/packer.spec.ts, zod-plugin/helpers.spec.ts, zod-plugin/index.spec.ts, zod-plugin/runtime.spec.ts
Added tests for new brand and packer utilities, updated test imports, and removed obsolete helper tests.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ZodSchema
    participant packer
    participant brand

    User->>ZodSchema: create schema
    User->>packer: pack(schema, { brand })
    packer->>ZodSchema: attach bag (metadata)
    User->>brand: setBrand.call(schema, brand)
    brand->>packer: pack(schema, { brand })
    User->>brand: getBrand(schema)
    brand->>packer: unpack(schema)
    packer-->>brand: return bag.brand
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

Suggested labels

breaking

Poem

A rabbit hops through fields of code,
Old brands and helpers now offload.
With packers’ bags and remap’s might,
Zod schemas gleam both day and night.
Tests abound, the toolkit grows—
In metadata, magic flows!
🐇✨


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2a37666 and 47fefc9.

📒 Files selected for processing (3)
  • zod-plugin/CHANGELOG.md (1 hunks)
  • zod-plugin/README.md (2 hunks)
  • zod-plugin/packer.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • zod-plugin/README.md
  • zod-plugin/CHANGELOG.md
  • zod-plugin/packer.ts
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch plugin-abstract-bag

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@RobinTail RobinTail marked this pull request as ready for review August 8, 2025 11:42
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: 2

🧹 Nitpick comments (10)
zod-plugin/packer.ts (5)

13-15: Mark $Packer as internal to avoid leaking implementation details

This interface is an internal check shape; exporting it expands the public API surface unnecessarily. Either remove export or mark it @internal so it won’t be relied upon by consumers.

-export interface $Packer<B extends object> extends z.core.$ZodCheck {
+/** @internal */
+interface $Packer<B extends object> extends z.core.$ZodCheck {
   _zod: $PackerInternals<B>;
 }

17-21: Polish the JSDoc: keep it impersonal and version-agnostic

Avoid personal references and hard-coded version tags in source comments; keep it about intent/behavior. Suggest concise wording:

-/**
- * This approach was suggested to me by Colin in a PM on Twitter.
- * Refrained from using Metadata because the data should withstand refinements.
- * @since 1.1.0 I generalized it to accept any object instead of only brand.
- * */
+/**
+ * Attaches an arbitrary metadata "bag" to a schema using a no-op $ZodCheck.
+ * We avoid vanilla Zod metadata so the data survives refinements/describe/etc.
+ */

22-36: Preserve existing bag type in the return and align the generic with z.core.$ZodType

Two improvements:

  • Return type should reflect that we’re merging with the existing bag, not replacing it.
  • Using z.core.$ZodType matches how the plugin API is surfaced and clarifies .check() availability.
-export const pack = <T extends z.ZodType, B extends object>(
+export const pack = <T extends z.core.$ZodType, B extends object>(
   subject: T,
   bag: B,
 ) => {
   const Cls = z.core.$constructor<$Packer<B>>("$Packer", (inst, def) => {
     z.core.$ZodCheck.init(inst, def);
     inst._zod.onattach.push((schema) => {
-      Object.assign(schema._zod.bag, def.bag);
+      // Defensive init (in case bag is absent on some exotic Zod internals)
+      // and then merge the provided bag.
+      if (!schema._zod.bag) schema._zod.bag = {};
+      Object.assign(schema._zod.bag, def.bag);
     });
     inst._zod.check = () => {};
   });
-  return subject.check(new Cls({ check: "$Packer", bag })) as T & {
-    _zod: { bag: B };
-  };
+  return subject.check(new Cls({ check: "$Packer", bag })) as T & {
+    _zod: { bag: T["_zod"]["bag"] & B };
+  };
 };

26-33: Defensive bag initialization in onattach

If schema._zod.bag is ever undefined for any schema shape, Object.assign would throw. A tiny guard keeps this robust across Zod internals.

-    inst._zod.onattach.push((schema) => {
-      Object.assign(schema._zod.bag, def.bag);
-    });
+    inst._zod.onattach.push((schema) => {
+      if (!schema._zod.bag) schema._zod.bag = {};
+      Object.assign(schema._zod.bag, def.bag);
+    });

38-40: Overload unpack() to propagate the bag type when known

Providing an overload makes unpack() return the precise B when the input carries that information, while keeping the fallback signature.

-export const unpack = <T extends z.core.$ZodType>(
-  subject: T,
-): T["_zod"]["bag"] => subject._zod.bag;
+export function unpack<B extends object>(
+  subject: z.core.$ZodType & { _zod: { bag: B } },
+): B;
+export function unpack<T extends z.core.$ZodType>(
+  subject: T,
+): T["_zod"]["bag"];
+export function unpack(subject: z.core.$ZodType) {
+  return subject._zod.bag;
+}
zod-plugin/packer.spec.ts (1)

13-21: Consider adding tests for repeated packing and precedence

Two extra checks would harden expectations:

  • Packing twice with overlapping keys should ensure “last write wins”.
  • Packing must not affect parse semantics.

Example additions:

test("repeated pack merges with last write wins", () => {
  const s1 = pack(z.string(), { a: 1, b: 1 });
  const s2 = pack(s1, { b: 2, c: 3 });
  expect(s2._zod.bag).toMatchObject({ a: 1, b: 2, c: 3 });
});

test("pack does not change parse behavior", () => {
  const base = z.string().min(2);
  const packed = pack(base, { tag: "x" });
  expect(() => packed.parse("x")).toThrow(); // still fails min(2)
  expect(packed.parse("ok")).toBe("ok");
});
zod-plugin/brand.spec.ts (2)

14-18: Restore the spy to avoid cross-test leakage

packMock remains active after the test completes, which can cause false positives/negatives in later suites that also spy on packer.pack.
Add a cleanup step:

 test("calls pack() with given brand", () => {
   const packMock = vi.spyOn(packer, "pack");
   ...
   expect(packMock).toHaveBeenCalledWith(schema, { brand: "test" });
+
+  packMock.mockRestore();
 });

16-16: Drop the optional-chaining – setBrand is always defined

setBrand is a named import from ./brand; if it were undefined the test (and runtime) would already throw.
Removing ?. clarifies intent and lets TypeScript catch accidental renames.

- setBrand?.call(schema, "test");
+ setBrand.call(schema, "test");
zod-plugin/brand.ts (2)

5-5: Mark the constant as a literal with as const

This preserves the string literal type "brand" instead of broadening to string, improving
type-safety for callers that use keyof tricks.

-export const brandProperty = "brand";
+export const brandProperty = "brand" as const;

8-10: Avoid writing an explicit undefined entry

Storing the key when brand is undefined clutters the internal bag and forces
downstream checks to handle an additional case. Skip the property altogether:

-export const setBrand = function (this: z.ZodType, brand?: PropertyKey) {
-  return pack(this, { [brandProperty]: brand });
+export const setBrand = function (this: z.ZodType, brand?: PropertyKey) {
+  return brand === undefined
+    ? this
+    : pack(this, { [brandProperty]: brand });
 };
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eab3b15 and 278241f.

📒 Files selected for processing (12)
  • zod-plugin/brand-check.ts (0 hunks)
  • zod-plugin/brand.spec.ts (1 hunks)
  • zod-plugin/brand.ts (1 hunks)
  • zod-plugin/helpers.spec.ts (0 hunks)
  • zod-plugin/helpers.ts (0 hunks)
  • zod-plugin/index.spec.ts (1 hunks)
  • zod-plugin/index.ts (1 hunks)
  • zod-plugin/packer.spec.ts (1 hunks)
  • zod-plugin/packer.ts (1 hunks)
  • zod-plugin/remap.ts (2 hunks)
  • zod-plugin/runtime.spec.ts (1 hunks)
  • zod-plugin/runtime.ts (3 hunks)
💤 Files with no reviewable changes (3)
  • zod-plugin/helpers.spec.ts
  • zod-plugin/helpers.ts
  • zod-plugin/brand-check.ts
🧰 Additional context used
🧠 Learnings (17)
📓 Common learnings
Learnt from: RobinTail
PR: RobinTail/express-zod-api#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
PR: RobinTail/express-zod-api#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
PR: RobinTail/express-zod-api#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.
Learnt from: RobinTail
PR: RobinTail/express-zod-api#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
PR: RobinTail/express-zod-api#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.
📚 Learning: 2025-08-01T09:48:13.742Z
Learnt from: RobinTail
PR: RobinTail/express-zod-api#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/runtime.spec.ts
  • zod-plugin/index.spec.ts
  • zod-plugin/index.ts
  • zod-plugin/runtime.ts
  • zod-plugin/packer.spec.ts
  • zod-plugin/brand.spec.ts
  • zod-plugin/brand.ts
  • zod-plugin/remap.ts
  • zod-plugin/packer.ts
📚 Learning: 2025-05-28T18:58:10.064Z
Learnt from: RobinTail
PR: RobinTail/express-zod-api#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:

  • zod-plugin/runtime.spec.ts
  • zod-plugin/index.spec.ts
  • zod-plugin/index.ts
  • zod-plugin/runtime.ts
  • zod-plugin/packer.spec.ts
  • zod-plugin/brand.spec.ts
  • zod-plugin/brand.ts
  • zod-plugin/remap.ts
  • zod-plugin/packer.ts
📚 Learning: 2025-06-14T16:42:52.972Z
Learnt from: RobinTail
PR: RobinTail/express-zod-api#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:

  • zod-plugin/runtime.spec.ts
  • zod-plugin/index.spec.ts
  • zod-plugin/index.ts
  • zod-plugin/runtime.ts
  • zod-plugin/brand.ts
  • zod-plugin/remap.ts
📚 Learning: 2025-05-27T19:35:57.357Z
Learnt from: RobinTail
PR: RobinTail/express-zod-api#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:

  • zod-plugin/runtime.spec.ts
  • zod-plugin/index.spec.ts
  • zod-plugin/index.ts
  • zod-plugin/runtime.ts
  • zod-plugin/packer.spec.ts
  • zod-plugin/brand.spec.ts
📚 Learning: 2025-05-27T20:27:17.015Z
Learnt from: RobinTail
PR: RobinTail/express-zod-api#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/runtime.spec.ts
  • zod-plugin/index.spec.ts
  • zod-plugin/runtime.ts
  • zod-plugin/remap.ts
📚 Learning: 2025-05-27T20:27:17.015Z
Learnt from: RobinTail
PR: RobinTail/express-zod-api#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:

  • zod-plugin/runtime.spec.ts
  • zod-plugin/index.ts
  • zod-plugin/runtime.ts
  • zod-plugin/packer.spec.ts
  • zod-plugin/brand.spec.ts
  • zod-plugin/brand.ts
  • zod-plugin/remap.ts
📚 Learning: 2025-05-27T20:03:34.213Z
Learnt from: RobinTail
PR: RobinTail/express-zod-api#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/runtime.spec.ts
  • zod-plugin/runtime.ts
📚 Learning: 2025-05-27T19:27:13.492Z
Learnt from: RobinTail
PR: RobinTail/express-zod-api#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/runtime.spec.ts
  • zod-plugin/index.ts
  • zod-plugin/runtime.ts
  • zod-plugin/remap.ts
📚 Learning: 2025-05-27T20:22:30.428Z
Learnt from: RobinTail
PR: RobinTail/express-zod-api#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/runtime.spec.ts
  • zod-plugin/index.spec.ts
  • zod-plugin/runtime.ts
  • zod-plugin/packer.spec.ts
  • zod-plugin/brand.spec.ts
📚 Learning: 2025-06-02T21:08:56.475Z
Learnt from: RobinTail
PR: RobinTail/express-zod-api#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:

  • zod-plugin/index.spec.ts
  • zod-plugin/index.ts
  • zod-plugin/packer.spec.ts
  • zod-plugin/brand.spec.ts
📚 Learning: 2025-05-28T07:58:09.853Z
Learnt from: RobinTail
PR: RobinTail/express-zod-api#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/runtime.ts
  • zod-plugin/remap.ts
📚 Learning: 2025-05-27T20:40:19.548Z
Learnt from: RobinTail
PR: RobinTail/express-zod-api#2546
File: express-zod-api/src/json-schema-helpers.ts:75-87
Timestamp: 2025-05-27T20:40:19.548Z
Learning: In express-zod-api's `flattenIO` function in json-schema-helpers.ts, the `additionalProperties` field is used as a template to generate property schemas for literal property names extracted from `propertyNames.const` and `propertyNames.enum`. Converting boolean `additionalProperties` values to empty objects `{}` via `Object(entry.additionalProperties)` is intentional behavior, as the function only needs property schema templates, not the boolean semantics of `additionalProperties`.

Applied to files:

  • zod-plugin/runtime.ts
  • zod-plugin/brand.ts
  • zod-plugin/remap.ts
📚 Learning: 2025-05-27T20:08:50.699Z
Learnt from: RobinTail
PR: RobinTail/express-zod-api#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/runtime.ts
  • zod-plugin/brand.spec.ts
  • zod-plugin/brand.ts
📚 Learning: 2025-05-28T05:04:40.327Z
Learnt from: RobinTail
PR: RobinTail/express-zod-api#2546
File: express-zod-api/src/io-schema.ts:5-8
Timestamp: 2025-05-28T05:04:40.327Z
Learning: The Base type `object & { [Symbol.iterator]?: never }` in express-zod-api correctly excludes arrays and other iterables from IOSchema. Arrays are properly rejected because they have a defined Symbol.iterator method which conflicts with the never constraint, as demonstrated by TypeScript compiler errors when attempting to assign z.array() to IOSchema.

Applied to files:

  • zod-plugin/runtime.ts
📚 Learning: 2025-07-20T11:09:58.980Z
Learnt from: RobinTail
PR: RobinTail/express-zod-api#2833
File: express-zod-api/src/method.ts:3-3
Timestamp: 2025-07-20T11:09:58.980Z
Learning: In express-zod-api, the `SomeMethod` type (defined as `Lowercase<string>`) is intentionally broad to represent raw, unvalidated HTTP methods from requests. The narrower `Method` type is used after validation with `isMethod()`. This defensive programming pattern separates raw external input types from validated types, allowing graceful handling of unknown methods.

Applied to files:

  • zod-plugin/runtime.ts
📚 Learning: 2025-05-27T19:30:51.885Z
Learnt from: RobinTail
PR: RobinTail/express-zod-api#2546
File: compat-test/sample.ts:1-1
Timestamp: 2025-05-27T19:30:51.885Z
Learning: Files in compat-test/ directories, especially those named sample.ts or similar, are often test fixtures for migration scripts and may intentionally contain deprecated or "incorrect" code that the migration tooling is designed to fix. These should not be flagged as issues.

Applied to files:

  • zod-plugin/packer.spec.ts
🧬 Code Graph Analysis (2)
zod-plugin/runtime.ts (2)
zod-plugin/brand.ts (1)
  • setBrand (8-10)
zod-plugin/remap.ts (1)
  • remap (30-42)
zod-plugin/brand.spec.ts (1)
zod-plugin/brand.ts (1)
  • setBrand (8-10)
🔇 Additional comments (6)
zod-plugin/runtime.spec.ts (1)

3-3: Import path change for getBrand is correct and no stale helper imports remain

I ran a search for any remaining imports from ./helpers and found none:

rg -n 'from\s+["'\'']\./helpers["'\'']|require\(["'\'']\./helpers["'\'']\)' || true
# no matches

Everything looks good—no further changes needed.

zod-plugin/index.spec.ts (1)

21-22: Export surface test updated appropriately

Asserting pack and unpack alongside getBrand reflects the new public API.

zod-plugin/index.ts (1)

3-4: Index exports are aligned with the refactor

Re-exporting pack/unpack and moving getBrand to "./brand" looks good. Side-effect imports remain intact.

zod-plugin/packer.spec.ts (2)

7-11: Good coverage of pack(): type and value behavior

Nice validation of both runtime bag content and type inference.


24-30: unpack() type assertion is on point

Verifying the inferred shape via expectTypeOf is great. With the suggested overloads, this should remain precise even when combining multiple packs.

zod-plugin/runtime.ts (1)

55-56: remap is added as a non-enumerable, non-configurable method – deliberate?

writable: false disallows hot-patching (useful in tests) and the default
enumerable: false hides the method from for…in / Object.keys. Confirm this
matches the public API expectations; otherwise set enumerable: true, configurable: true.

Comment thread zod-plugin/remap.ts
Comment thread zod-plugin/runtime.ts
@coveralls-official
Copy link
Copy Markdown

coveralls-official Bot commented Aug 8, 2025

Coverage Status

coverage: 100.0%. remained the same
when pulling 47fefc9 on plugin-abstract-bag
into 35062f4 on master.

Repository owner deleted a comment from coveralls-official Bot Aug 8, 2025
Repository owner deleted a comment from coveralls-official Bot Aug 8, 2025
Repository owner deleted a comment from coveralls-official Bot Aug 8, 2025
@RobinTail
Copy link
Copy Markdown
Owner Author

8-10: Avoid writing an explicit undefined entry

@coderabbitai, this suggestion is not correct, because I must ensure that brand setter is immutable (always calls .check())

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Aug 8, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@RobinTail Thanks for the correction—acknowledged. For the brand setter to remain immutable and always trigger .check() via the packer, keeping the explicit undefined entry makes sense so the onattach path runs consistently. I’ll avoid suggesting its removal in this context.


🏁 Script executed:

#!/bin/bash
# Inspect files likely related to "brand setter" and lines 8-10.
echo "== zod-plugin/brand.ts =="
nl -ba $(fd -a 'brand.ts' | rg 'zod-plugin/brand.ts' -n | cut -d: -f1) | sed -n '1,120p'

echo "== zod-plugin/runtime.ts =="
nl -ba $(fd -a 'runtime.ts' | rg 'zod-plugin/runtime.ts' -n | cut -d: -f1) | sed -n '1,120p'

echo "== zod-plugin/packer.ts =="
nl -ba $(fd -a 'packer.ts' | rg 'zod-plugin/packer.ts' -n | cut -d: -f1) | sed -n '1,120p'

Length of output: 741


@RobinTail You’re right—keeping the explicit undefined entry is necessary here. It ensures:

  • The setter remains immutable by always invoking pack → .check() (onattach runs).
  • Calling brand(undefined) actually overrides any existing brand (unbrands), which a “skip undefined key” optimization would break.

I’ll avoid suggesting that change in this context. Thanks for the clarification.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate key symbols =="
rg -n --color=never -e 'brandProperty|setBrand|getBrand|pack\(|unpack\(|Object\.defineProperty|ZodType\.prototype\.brand|remap' || true

echo
echo "== zod-plugin/brand.ts (first 120 lines) =="
fd -a 'brand.ts' | rg 'zod-plugin/brand.ts' -n | cut -d: -f1 | xargs -I{} sh -c 'awk "NR>=1 && NR<=120 {print NR\":\"\$0}" "{}"'

echo
echo "== zod-plugin/runtime.ts (first 140 lines) =="
fd -a 'runtime.ts' | rg 'zod-plugin/runtime.ts' -n | cut -d: -f1 | xargs -I{} sh -c 'awk "NR>=1 && NR<=140 {print NR\":\"\$0}" "{}"'

echo
echo "== zod-plugin/packer.ts (first 160 lines) =="
fd -a 'packer.ts' | rg 'zod-plugin/packer.ts' -n | cut -d: -f1 | xargs -I{} sh -c 'awk "NR>=1 && NR<=160 {print NR\":\"\$0}" "{}"'

echo
echo "== zod-plugin/brand.spec.ts (first 120 lines) =="
fd -a 'brand.spec.ts' | rg 'zod-plugin/brand.spec.ts' -n | cut -d: -f1 | xargs -I{} sh -c 'awk "NR>=1 && NR<=120 {print NR\":\"\$0}" "{}"'

Length of output: 9318


@RobinTail Agreed. The code confirms this:

  • In zod-plugin/brand.ts Line 8–9, setBrand returns pack(this, { [brandProperty]: brand }); keeping the key even when brand is undefined.
  • In zod-plugin/runtime.ts Line 41, the brand getter returns setBrand.bind(this), so every call must go through pack → .check() to remain immutable.
  • Packer merges bags in onattach, so including brand: undefined also overwrites any prior brand (unbrands).

I’ll refrain from suggesting removal of the explicit undefined entry here. Also noted this aligns with our earlier learning about not memoizing the brand getter.

(•̀ᴗ•́)و ̑̑


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Helper to print a file with line numbers up to a limit
print_head() {
  local file="$1"
  local limit="${2:-120}"
  if [ -n "$file" ] && [ -f "$file" ]; then
    echo "== $file (1..$limit) =="
    awk -v max="$limit" '{ printf "%d: %s\n", NR, $0; if (NR>=max) exit }' "$file"
    echo
  fi
}

# Locate files
brand_file="$(fd -a -t f 'zod-plugin/brand.ts' | head -n1 || true)"
runtime_file="$(fd -a -t f 'zod-plugin/runtime.ts' | head -n1 || true)"
packer_file="$(fd -a -t f 'zod-plugin/packer.ts' | head -n1 || true)"
brand_spec_file="$(fd -a -t f 'zod-plugin/brand.spec.ts' | head -n1 || true)"

# Show small heads
print_head "$brand_file" 40
print_head "$runtime_file" 80
print_head "$packer_file" 160
print_head "$brand_spec_file" 120

# Also, pinpoint lines of interest with ripgrep for quick reference
echo "== Quick references =="
rg -n --color=never -e 'brandProperty|setBrand|getBrand|onattach|_zod\.bag|ZodType\.prototype\.brand' "$brand_file" "$runtime_file" "$packer_file" || true

Length of output: 841


@RobinTail You’re right—keeping the explicit undefined entry is necessary:

  • zod-plugin/brand.ts Lines 8–9 show setBrand returning pack(this, { [brandProperty]: brand }); which must include the brand key even when undefined to “unbrand” and still trigger .check() via pack.
  • zod-plugin/runtime.ts Line 41 binds the getter to setBrand, so immutability relies on always going through pack → .check().

I’ll withdraw that suggestion and won’t flag this pattern again. Noted alongside our prior learning about not memoizing the brand getter.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

show() {
  local f="$1" ; local n="${2:-140}"
  if [ -f "$f" ]; then
    echo "== $f (1..$n) =="
    awk -v max="$n" '{ printf "%d: %s\n", NR, $0; if (NR>=max) exit }' "$f"
    echo
  else
    echo "MISSING: $f"
    fd -a -t f "$(basename "$f")" || true
    echo
  fi
}

show zod-plugin/brand.ts 60
show zod-plugin/runtime.ts 120
show zod-plugin/packer.ts 200

echo "== onattach/check references in packer.ts =="
rg -n --color=never -e 'onattach|check\\(' zod-plugin/packer.ts || true

Length of output: 5063


@RobinTail You’re right—keeping the explicit undefined is required to preserve immutability and unbranding:

  • zod-plugin/brand.ts Line 8–9: setBrand calls pack(this, { [brandProperty]: brand }) so the key is always written.
  • zod-plugin/runtime.ts Line 39–42: the brand getter binds to setBrand, so every call must go through pack → .check().
  • zod-plugin/packer.ts Line 27–34: onattach merges the bag into _zod.bag, so brand: undefined correctly overwrites any prior brand.

I’m withdrawing that suggestion and won’t flag this pattern again. This also aligns with your earlier guidance to avoid memoizing the brand getter.


✏️ Learnings added
Learnt from: RobinTail
URL: https://github.com/RobinTail/express-zod-api/pull/2878

Timestamp: 2025-08-08T16:45:20.500Z
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
URL: https://github.com/RobinTail/express-zod-api/pull/2878

Timestamp: 2025-08-08T16:45:20.500Z
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
URL: https://github.com/RobinTail/express-zod-api/pull/2878

Timestamp: 2025-08-08T16:45:20.500Z
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
URL: https://github.com/RobinTail/express-zod-api/pull/2878

Timestamp: 2025-08-08T16:45:20.500Z
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.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: RobinTail
PR: RobinTail/express-zod-api#2878
File: zod-plugin/runtime.ts:39-42
Timestamp: 2025-08-08T11:59:04.790Z
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
PR: RobinTail/express-zod-api#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.

Learnt from: RobinTail
PR: RobinTail/express-zod-api#2546
File: express-zod-api/src/result-handler.ts:154-168
Timestamp: 2025-05-27T21:03:26.944Z
Learning: In JavaScript object literals, spreading `undefined` or `null` does not cause runtime errors. `{ ...undefined }` evaluates to `{}` and `{ ...meta, examples }` where `meta` is `undefined` evaluates to `{ examples: [...] }`. The spread operator gracefully ignores `undefined` and `null` values.

Learnt from: RobinTail
PR: RobinTail/express-zod-api#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.

@RobinTail RobinTail added the documentation Improvements or additions to documentation label Aug 8, 2025
@RobinTail RobinTail changed the title Feat: abstract methods for Zod Plugin feat(zod-plugin): Abstract methods pack() and unpack() Aug 8, 2025
@RobinTail
Copy link
Copy Markdown
Owner Author

RobinTail commented Aug 9, 2025

✅ worked well for zod-sockets
✅ regression OK

✅ QA passed

Copy link
Copy Markdown
Owner Author

@RobinTail RobinTail left a comment

Choose a reason for hiding this comment

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

🏁 Ready

@RobinTail RobinTail merged commit e476d73 into master Aug 9, 2025
12 checks passed
@RobinTail RobinTail deleted the plugin-abstract-bag branch August 9, 2025 17:02
RobinTail added a commit that referenced this pull request Oct 27, 2025
#2878 (comment)

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

* **Bug Fixes**
* Remap now detects and rejects duplicate target keys in mappings to
prevent key collisions.

* **Tests**
* Added a runtime test asserting an error is thrown for duplicate
mapping targets.

* **Documentation**
* CHANGELOG updated to document the remap duplicate-target fix (v2.1.0).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request refactoring The better way to achieve the same result

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant