fix(to-valibot): support z.brand() in codemod (#1501)#1542
Conversation
The codemod did not recognize Zod's .brand() method, so
z.string().brand("X") was emitted as the broken v.string()("X").
Add a brand method handler that maps it into a pipe action,
producing v.pipe(v.string(), v.brand("X")), matching how the
existing transform method is handled.
Fixes open-circle#1501
WalkthroughThe Zod-to-Valibot codemod now recognizes the 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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
`@codemod/zod-to-valibot/src/transform/schemas-and-links/methods/brand/brand.ts`:
- Around line 4-8: Add JSDoc immediately above the exported transformBrand
function describing its purpose, and add the // `@__NO_SIDE_EFFECTS__` annotation
required for this pure factory transformation. Keep the function signature and
implementation unchanged.
- Around line 1-2: Update the relative ESM specifiers in brand.ts,
brand/index.ts, and methods/index.ts to include the .ts extension: use
../../helpers.ts, ./brand.ts, and ./brand.ts respectively. Leave the jscodeshift
package import unchanged.
- Around line 4-20: Update transformBrand to handle Zod’s type-only brand<...>()
form instead of forwarding only runtime args: preserve the brand name by
converting the type argument to the required Valibot name, or explicitly reject
unsupported type-only branding. Add a fixture covering
z.string().brand<"UserId">() and verify the resulting transform behavior.
🪄 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: 2ecf73f0-45e6-4781-ac57-4211df4fe6eb
📒 Files selected for processing (8)
codemod/zod-to-valibot/__testfixtures__/brand/input.tscodemod/zod-to-valibot/__testfixtures__/brand/output.tscodemod/zod-to-valibot/src/test-setup.test.tscodemod/zod-to-valibot/src/transform/schemas-and-links/constants.tscodemod/zod-to-valibot/src/transform/schemas-and-links/methods/brand/brand.tscodemod/zod-to-valibot/src/transform/schemas-and-links/methods/brand/index.tscodemod/zod-to-valibot/src/transform/schemas-and-links/methods/index.tscodemod/zod-to-valibot/src/transform/schemas-and-links/schemas-and-links.ts
| import j from 'jscodeshift'; | ||
| import { addToPipe } from '../../helpers'; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Use .ts extensions on the new relative imports and exports.
As per coding guidelines, ESM relative specifiers in these TypeScript files must include .ts; keep the package import unchanged.
codemod/zod-to-valibot/src/transform/schemas-and-links/methods/brand/brand.ts#L1-L2: change../../helpersto../../helpers.ts.codemod/zod-to-valibot/src/transform/schemas-and-links/methods/brand/index.ts#L1: change./brandto./brand.ts.codemod/zod-to-valibot/src/transform/schemas-and-links/methods/index.ts#L2: change./brandto./brand.ts.
📍 Affects 3 files
codemod/zod-to-valibot/src/transform/schemas-and-links/methods/brand/brand.ts#L1-L2(this comment)codemod/zod-to-valibot/src/transform/schemas-and-links/methods/brand/index.ts#L1-L1codemod/zod-to-valibot/src/transform/schemas-and-links/methods/index.ts#L2-L2
🤖 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
`@codemod/zod-to-valibot/src/transform/schemas-and-links/methods/brand/brand.ts`
around lines 1 - 2, Update the relative ESM specifiers in brand.ts,
brand/index.ts, and methods/index.ts to include the .ts extension: use
../../helpers.ts, ./brand.ts, and ./brand.ts respectively. Leave the jscodeshift
package import unchanged.
Source: Coding guidelines
| export function transformBrand( | ||
| valibotIdentifier: string, | ||
| schemaExp: j.CallExpression | j.MemberExpression | j.Identifier, | ||
| args: j.CallExpression['arguments'] | ||
| ) { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Add the required JSDoc and purity annotation.
This exported function is under codemod/** but has neither JSDoc nor // @NO_SIDE_EFFECTS``. As per coding guidelines, exported functions require JSDoc and pure factory functions require the annotation.
🤖 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
`@codemod/zod-to-valibot/src/transform/schemas-and-links/methods/brand/brand.ts`
around lines 4 - 8, Add JSDoc immediately above the exported transformBrand
function describing its purpose, and add the // `@__NO_SIDE_EFFECTS__` annotation
required for this pure factory transformation. Keep the function signature and
implementation unchanged.
Source: Coding guidelines
| export function transformBrand( | ||
| valibotIdentifier: string, | ||
| schemaExp: j.CallExpression | j.MemberExpression | j.Identifier, | ||
| args: j.CallExpression['arguments'] | ||
| ) { | ||
| return addToPipe( | ||
| valibotIdentifier, | ||
| schemaExp, | ||
| j.callExpression( | ||
| j.memberExpression( | ||
| j.identifier(valibotIdentifier), | ||
| j.identifier('brand') | ||
| ), | ||
| args | ||
| ) | ||
| ); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '--- brand transformer ---\n'
sed -n '1,220p' codemod/zod-to-valibot/src/transform/schemas-and-links/methods/brand/brand.ts
printf '\n--- nearby tests/fixtures ---\n'
fd -a 'brand' codemod/zod-to-valibot -t f
fd -a 'brand' codemod/zod-to-valibot -t f | while read -r f; do
echo "\n### $f"
sed -n '1,220p' "$f"
done
printf '\n--- search for brand() usage and docs ---\n'
rg -n "brand<|brand\(" codemod/zod-to-valibot packages library -g '!**/dist/**' -g '!**/build/**'Repository: open-circle/valibot
Length of output: 248
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- brand transformer ---'
sed -n '1,220p' codemod/zod-to-valibot/src/transform/schemas-and-links/methods/brand/brand.ts
echo
echo '--- nearby tests/fixtures ---'
fd -a 'brand' codemod/zod-to-valibot -t f
fd -a 'brand' codemod/zod-to-valibot -t f | while read -r f; do
echo
echo "### $f"
sed -n '1,240p' "$f"
done
echo
echo '--- search for brand usage and valibot brand API ---'
rg -n "brand<|brand\(" codemod/zod-to-valibot packages library -g '!**/dist/**' -g '!**/build/**'Repository: open-circle/valibot
Length of output: 2735
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- search for brand type-parameter handling ---'
rg -n "typeParameters|typeArguments|TSLiteralType|brand" codemod/zod-to-valibot/src codemod/zod-to-valibot/__testfixtures__ -g '!**/dist/**' -g '!**/build/**'
echo
echo '--- parser/transform entrypoints around brand ---'
fd -a 'brand' codemod/zod-to-valibot/src -t f
fd -a 'schema' codemod/zod-to-valibot/src -t fRepository: open-circle/valibot
Length of output: 3646
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- schemas-and-links brand dispatch ---'
sed -n '350,430p' codemod/zod-to-valibot/src/transform/schemas-and-links/schemas-and-links.ts
echo
echo '--- surrounding helper signatures ---'
sed -n '1,120p' codemod/zod-to-valibot/src/transform/schemas-and-links/methods/index.ts
sed -n '1,120p' codemod/zod-to-valibot/src/transform/schemas-and-links/methods/brand/index.ts
echo
echo '--- pure factory / JSDoc examples in codemod ---'
rg -n "// `@__NO_SIDE_EFFECTS__`|export function" codemod/zod-to-valibot/src -g '!**/dist/**' -g '!**/build/**'Repository: open-circle/valibot
Length of output: 16913
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- Valibot brand signature ---'
sed -n '1,120p' library/src/actions/brand/brand.ts
echo
echo '--- brand test coverage ---'
sed -n '1,120p' library/src/actions/brand/brand.test.ts
sed -n '1,120p' library/src/actions/brand/brand.test-d.tsRepository: open-circle/valibot
Length of output: 3363
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- schema-and-links method parsing ---'
sed -n '120,240p' codemod/zod-to-valibot/src/transform/schemas-and-links/schemas-and-links.ts
echo
echo '--- brand test fixtures, if any type-only case exists elsewhere ---'
rg -n "brand<|\.brand\(" codemod/zod-to-valibot/__testfixtures__ -g '!**/dist/**' -g '!**/build/**'Repository: open-circle/valibot
Length of output: 5188
🌐 Web query:
Zod brand type parameter brand<T>() Valibot brand requires name argument API docs
💡 Result:
In Valibot, the brand function is a transformation action used to brand the output type of a schema with a specific name [1][2]. Regarding your query about the API requirements: yes, the brand function requires a name argument [2][3]. Its signature is defined as: brand<TInput, TName extends BrandName>(name: TName): BrandAction<TInput, TName> The name argument (of type TName) is mandatory and is used to uniquely identify the brand at the type level [2][3]. This branding ensures that data can only be considered valid if it has been validated by a schema containing that specific brand [1]. Unlike some other libraries where branding might be purely a type-level construct, Valibot's brand acts as a runtime transformation action [2][3].
Citations:
- 1: https://valibot.dev/api/brand/
- 2: https://jsr.io/@valibot/valibot/doc/all_symbols
- 3: https://jsr.io/@valibot/valibot/doc/~/brand
🌐 Web query:
Zod docs brand type-only brand<"UserId">() syntax
💡 Result:
In Zod, the.brand<"Name"> syntax is a static-only construct used to create nominal types by "stamping" a Zod schema [1][2][3]. It does not affect the runtime behavior of.parse or.safeParse; its primary purpose is to distinguish between types that share the same underlying structure (e.g., distinguishing a UserId from a regular string) at the TypeScript type level [1][4][3]. Usage Syntax: You apply the brand to a Zod schema using the.brand method, passing the brand name as a string literal generic [3][5]. const UserIdSchema = z.string.brand<"UserId">; type UserId = z.infer; // string & z.$brand<"UserId"> In newer versions (Zod 4.x), you can optionally specify the direction of the brand to control whether it applies to the input, output, or both [3][5]: z.string.brand<"UserId", "in">; // Brand applies to input z.string.brand<"UserId", "out">; // Brand applies to output z.string.brand<"UserId", "inout">; // Brand applies to both (default) Key Characteristics: - Type-Only: Branding exists purely at the TypeScript type level [1][6]. Zod does not use these brands during runtime validation [4][7]. - Persistence: Brands are generally preserved through standard Zod operations, but they can be lost when using.transform or.pipe because these operations create new types that TypeScript may not automatically associate with the original brand [7][6]. If a brand is lost through these operations, you must manually re-apply it [7][6]. - Nominal Typing: By using branded types, you prevent accidental interchangeability of types. For example, a function expecting a UserId will reject a plain string or a string branded as a different type, even if both are structurally strings [3][5].
Citations:
- 1: https://zod.dev/api
- 2: https://v3.zod.dev/?id=or
- 3: https://dev.to/tumf/implementing-the-newtype-pattern-with-zod-enhancing-type-safety-in-typescript-5c62
- 4: https://spin.atomicobject.com/zod-brand/
- 5: https://dev.to/gabrielanhaia/stop-passing-string-for-user-ids-branded-types-arent-optional-anymore-229p
- 6: Branding gets dropped in zod v4 from transforms and pipes colinhacks/zod#5648
- 7: [Zod v4] Branded Types Lose Their Brand When Piped After Transformation colinhacks/zod#5183
Handle Zod's type-only brand<...>() form here.
transformBrand only forwards runtime args, so z.string().brand<"UserId">() drops the brand name and becomes v.brand(), but Valibot requires a name. Carry the type argument through or reject this form explicitly, and add a fixture for it.
🤖 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
`@codemod/zod-to-valibot/src/transform/schemas-and-links/methods/brand/brand.ts`
around lines 4 - 20, Update transformBrand to handle Zod’s type-only
brand<...>() form instead of forwarding only runtime args: preserve the brand
name by converting the type argument to the required Valibot name, or explicitly
reject unsupported type-only branding. Add a fixture covering
z.string().brand<"UserId">() and verify the resulting transform behavior.
There was a problem hiding this comment.
1 issue found across 8 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="codemod/zod-to-valibot/src/transform/schemas-and-links/methods/brand/brand.ts">
<violation number="1" location="codemod/zod-to-valibot/src/transform/schemas-and-links/methods/brand/brand.ts:17">
P1: When Zod's `.brand()` is called with only a type argument (e.g., `z.string().brand<"UserId">()`), the `args` array will be empty. This produces `v.brand()` in the output, but Valibot's `brand` action requires a `name` parameter. The type-argument-only form is common in Zod codebases. Consider extracting the type argument from the AST node and using it as the brand name, or emitting a warning/comment when no runtime argument is provided.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| j.identifier(valibotIdentifier), | ||
| j.identifier('brand') | ||
| ), | ||
| args |
There was a problem hiding this comment.
P1: When Zod's .brand() is called with only a type argument (e.g., z.string().brand<"UserId">()), the args array will be empty. This produces v.brand() in the output, but Valibot's brand action requires a name parameter. The type-argument-only form is common in Zod codebases. Consider extracting the type argument from the AST node and using it as the brand name, or emitting a warning/comment when no runtime argument is provided.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At codemod/zod-to-valibot/src/transform/schemas-and-links/methods/brand/brand.ts, line 17:
<comment>When Zod's `.brand()` is called with only a type argument (e.g., `z.string().brand<"UserId">()`), the `args` array will be empty. This produces `v.brand()` in the output, but Valibot's `brand` action requires a `name` parameter. The type-argument-only form is common in Zod codebases. Consider extracting the type argument from the AST node and using it as the brand name, or emitting a warning/comment when no runtime argument is provided.</comment>
<file context>
@@ -0,0 +1,20 @@
+ j.identifier(valibotIdentifier),
+ j.identifier('brand')
+ ),
+ args
+ )
+ );
</file context>
Overview
The
@valibot/zod-to-valibotcodemod had no handler for Zod's.brand()method. Because the method went unrecognized, it was left in place as a call on the transformed schema, producing invalid output likev.string()("Name")(a schema object called as a function).Fixes #1501
Fix
Added a
brandmethod handler that mapsschema.brand(name)into a pipe action, mirroring how the existingtransformmethod is handled. It uses the sharedaddToPipehelper so the brand is appended to any existing pipe:The handler is wired into
ZOD_METHODS(constants),methods/index.ts, and the schemas-and-links dispatcher.Tests
Added a
brandtest fixture (__testfixtures__/brand/) covering a basic brand, a brand after a validator chain, a brand on a number schema, and a brand on a linked schema, and registered it intest-setup.test.ts. The full codemod suite passes (72/72).Summary by CodeRabbit