-
Notifications
You must be signed in to change notification settings - Fork 67
fix: optional transforms for zod4 output #889
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
# convex-helpers Zod4 Optional Codec Fix
## Problem
When using `zodOutputToConvex()` with an optional codec (e.g., `dateToMsCodec.optional()`), TypeScript incorrectly infers the type as `never` instead of `VOptional<VFloat64>`.
This happens because `ConvexValidatorFromZodOutput` uses `ConvexValidatorFromZod` (which looks at **input** types) for nested types like `$ZodOptional`, instead of using itself recursively (which looks at **output** types).
For codecs, the input type is something like `z.date()` which is not a valid Convex type, causing the type inference to fail.
## Files to Patch
- `node_modules/convex-helpers/server/zod4.ts`
- `node_modules/convex-helpers/server/zod4.d.ts`
---
## Patch for `zod4.ts`
### Location
Around line 1028-1048 (the `ConvexValidatorFromZodOutput` type definition)
### BEFORE (Original)
```typescript
/**
* Return type of {@link zodOutputToConvex}.
*/
export type ConvexValidatorFromZodOutput<
Z extends zCore.$ZodType,
IsOptional extends "required" | "optional",
> =
// `unknown` / `any`: we can't infer a precise return type at compile time
IsUnknownOrAny<Z> extends true
? GenericValidator
: // z.default()
Z extends zCore.$ZodDefault<infer Inner extends zCore.$ZodType> // output: always there
? VRequired<ConvexValidatorFromZod<Inner, "required">>
: // z.pipe()
Z extends zCore.$ZodPipe<
infer _Input extends zCore.$ZodType,
infer Output extends zCore.$ZodType
>
? ConvexValidatorFromZod<Output, IsOptional>
: // All other schemas have the same input/output types
ConvexValidatorFromZodCommon<Z, IsOptional>;
```
### AFTER (Patched)
```typescript
/**
* Return type of {@link zodOutputToConvex}.
*/
export type ConvexValidatorFromZodOutput<
Z extends zCore.$ZodType,
IsOptional extends "required" | "optional",
> =
// `unknown` / `any`: we can't infer a precise return type at compile time
IsUnknownOrAny<Z> extends true
? GenericValidator
: // z.default()
Z extends zCore.$ZodDefault<infer Inner extends zCore.$ZodType> // output: always there
? VRequired<ConvexValidatorFromZodOutput<Inner, "required">>
: // z.pipe() - use output schema for zodOutputToConvex
Z extends zCore.$ZodPipe<
infer _Input extends zCore.$ZodType,
infer Output extends zCore.$ZodType
>
? ConvexValidatorFromZodOutput<Output, IsOptional>
: // z.optional() - handle here to use output types consistently
Z extends zCore.$ZodOptional<infer Inner extends zCore.$ZodType>
? VOptional<ConvexValidatorFromZodOutput<Inner, "optional">>
: // z.nullable() - handle here to use output types consistently
Z extends zCore.$ZodNullable<infer Inner extends zCore.$ZodType>
? ConvexValidatorFromZodOutput<Inner, IsOptional> extends Validator<
any,
"optional",
any
>
? VUnion<
| ConvexValidatorFromZodOutput<Inner, IsOptional>["type"]
| null
| undefined,
[
VRequired<ConvexValidatorFromZodOutput<Inner, IsOptional>>,
VNull,
],
"optional",
ConvexValidatorFromZodOutput<Inner, IsOptional>["fieldPaths"]
>
: VUnion<
| ConvexValidatorFromZodOutput<Inner, IsOptional>["type"]
| null,
[
VRequired<ConvexValidatorFromZodOutput<Inner, IsOptional>>,
VNull,
],
IsOptional,
ConvexValidatorFromZodOutput<Inner, IsOptional>["fieldPaths"]
>
: // All other schemas have the same input/output types
ConvexValidatorFromZodCommon<Z, IsOptional>;
```
---
## Patch for `zod4.d.ts`
### Location
Line 464 (the `ConvexValidatorFromZodOutput` type declaration - single long line)
### BEFORE (Original)
```typescript
export type ConvexValidatorFromZodOutput<Z extends zCore.$ZodType, IsOptional extends "required" | "optional"> = IsUnknownOrAny<Z> extends true ? GenericValidator : Z extends zCore.$ZodDefault<infer Inner extends zCore.$ZodType> ? VRequired<ConvexValidatorFromZod<Inner, "required">> : Z extends zCore.$ZodPipe<infer _Input extends zCore.$ZodType, infer Output extends zCore.$ZodType> ? ConvexValidatorFromZod<Output, IsOptional> : ConvexValidatorFromZodCommon<Z, IsOptional>;
```
### AFTER (Patched)
```typescript
export type ConvexValidatorFromZodOutput<Z extends zCore.$ZodType, IsOptional extends "required" | "optional"> = IsUnknownOrAny<Z> extends true ? GenericValidator : Z extends zCore.$ZodDefault<infer Inner extends zCore.$ZodType> ? VRequired<ConvexValidatorFromZodOutput<Inner, "required">> : Z extends zCore.$ZodPipe<infer _Input extends zCore.$ZodType, infer Output extends zCore.$ZodType> ? ConvexValidatorFromZodOutput<Output, IsOptional> : Z extends zCore.$ZodOptional<infer Inner extends zCore.$ZodType> ? VOptional<ConvexValidatorFromZodOutput<Inner, "optional">> : Z extends zCore.$ZodNullable<infer Inner extends zCore.$ZodType> ? ConvexValidatorFromZodOutput<Inner, IsOptional> extends Validator<any, "optional", any> ? VUnion<ConvexValidatorFromZodOutput<Inner, IsOptional>["type"] | null | undefined, [VRequired<ConvexValidatorFromZodOutput<Inner, IsOptional>>, VNull], "optional", ConvexValidatorFromZodOutput<Inner, IsOptional>["fieldPaths"]> : VUnion<ConvexValidatorFromZodOutput<Inner, IsOptional>["type"] | null, [VRequired<ConvexValidatorFromZodOutput<Inner, IsOptional>>, VNull], IsOptional, ConvexValidatorFromZodOutput<Inner, IsOptional>["fieldPaths"]> : ConvexValidatorFromZodCommon<Z, IsOptional>;
```
---
## Key Changes Summary
1. **`ConvexValidatorFromZod` → `ConvexValidatorFromZodOutput`**: Changed all recursive calls within `ConvexValidatorFromZodOutput` to use itself instead of `ConvexValidatorFromZod`
2. **Added `$ZodOptional` handling**: Added explicit handling for `z.optional()` before falling through to `ConvexValidatorFromZodCommon`
3. **Added `$ZodNullable` handling**: Added explicit handling for `z.nullable()` before falling through to `ConvexValidatorFromZodCommon`
This ensures that when processing optional/nullable wrapped codecs, the type system correctly uses the **output** schema of the codec (e.g., `number`) rather than the **input** schema (e.g., `Date`).
---
## Testing
After applying the patch, the following should compile without errors:
```typescript
import z from "zod/v4";
import { zodOutputToConvex } from "convex-helpers/server/zod4";
const dateToMsCodec = z.codec(z.date(), z.int().min(0), {
encode: (millis: number) => new Date(millis),
decode: (date: Date) => date.getTime(),
});
const optionalCodec = dateToMsCodec.optional();
const validator = zodOutputToConvex(optionalCodec);
// These should work without "Property does not exist on type 'never'" errors
console.log(validator.kind); // "float64"
console.log(validator.isOptional); // "optional"
```
WalkthroughUpdated the Zod-to-Convex output validator mapping to use consistent output semantics. Modified Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: defaults Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used📓 Path-based instructions (1)**/*.{ts,tsx}📄 CodeRabbit inference engine (AGENTS.md)
Files:
🧬 Code graph analysis (1)packages/convex-helpers/server/zod4.ts (3)
🔇 Additional comments (1)
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 |
convex-helpers Zod4 Optional Codec Fix
Problem
When using
zodOutputToConvex()with an optional codec (e.g.,dateToMsCodec.optional()), TypeScript incorrectly infers the type asneverinstead ofVOptional<VFloat64>.This happens because
ConvexValidatorFromZodOutputusesConvexValidatorFromZod(which looks at input types) for nested types like$ZodOptional, instead of using itself recursively (which looks at output types).For codecs, the input type is something like
z.date()which is not a valid Convex type, causing the type inference to fail.Files to Patch
node_modules/convex-helpers/server/zod4.tsnode_modules/convex-helpers/server/zod4.d.tsPatch for
zod4.tsLocation
Around line 1028-1048 (the
ConvexValidatorFromZodOutputtype definition)BEFORE (Original)
AFTER (Patched)
Patch for
zod4.d.tsLocation
Line 464 (the
ConvexValidatorFromZodOutputtype declaration - single long line)BEFORE (Original)
AFTER (Patched)
Key Changes Summary
ConvexValidatorFromZod→ConvexValidatorFromZodOutput: Changed all recursive calls withinConvexValidatorFromZodOutputto use itself instead ofConvexValidatorFromZodAdded
$ZodOptionalhandling: Added explicit handling forz.optional()before falling through toConvexValidatorFromZodCommonAdded
$ZodNullablehandling: Added explicit handling forz.nullable()before falling through toConvexValidatorFromZodCommonThis ensures that when processing optional/nullable wrapped codecs, the type system correctly uses the output schema of the codec (e.g.,
number) rather than the input schema (e.g.,Date).Testing
After applying the patch, the following should compile without errors:
The following applies to third-party contributors.
Convex employees and contractors can delete or ignore.
-->
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.