From 7e895beaef0e34630e584ea78f916b236028c58f Mon Sep 17 00:00:00 2001 From: gary-ix Date: Sun, 16 Nov 2025 13:13:01 -0600 Subject: [PATCH 1/2] Fix zodOutputToConvex type inference for objects and unions --- packages/convex-helpers/server/zod3.test.ts | 54 +++++++++++++++++++++ packages/convex-helpers/server/zod3.ts | 29 ++++++++++- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/packages/convex-helpers/server/zod3.test.ts b/packages/convex-helpers/server/zod3.test.ts index 17c622d8..cbdf0a9e 100644 --- a/packages/convex-helpers/server/zod3.test.ts +++ b/packages/convex-helpers/server/zod3.test.ts @@ -618,6 +618,60 @@ test("output validators work for arrays objects and unions", async () => { expect(union.members[1].isOptional).toBe("required"); }); +test("zodOutputToConvex infers required types for fields with defaults", () => { + const ZSimpleObject = z.object({ + age: z.number().default(25), + name: z.string(), + status: z.union([z.literal("active"), z.literal("inactive")]).default("active"), + }); + const _VSimpleObject = zodOutputToConvex(ZSimpleObject); + type TZodSimpleObject = z.infer; + type TConvexSimpleObject = Infer; + + // Fields with defaults should be required (not optional) in the output type + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + TZodSimpleObject["status"] + >(); + expectTypeOf().toEqualTypeOf(); + + // Validator objects should have isOptional: "required" for fields with defaults + expect(_VSimpleObject.fields.age.isOptional).toBe("required"); + expect(_VSimpleObject.fields.status.isOptional).toBe("required"); + expect(_VSimpleObject.fields.name.isOptional).toBe("required"); + + // Test nested objects with defaults + const ZNestedObject = z.object({ + id: z.string(), + profile: z.object({ + displayName: z.string().default("Anonymous"), + isPublic: z.boolean().default(false), + }), + tags: z.array(z.string()).default([]), + }); + const _VNestedObject = zodOutputToConvex(ZNestedObject); + type TZodNestedObject = z.infer; + type TConvexNestedObject = Infer; + + // Nested object fields with defaults should be required + expectTypeOf().toEqualTypeOf< + TZodNestedObject["tags"] + >(); + expectTypeOf().toEqualTypeOf< + TZodNestedObject["profile"]["displayName"] + >(); + expectTypeOf().toEqualTypeOf< + TZodNestedObject["profile"]["isPublic"] + >(); + expectTypeOf().toEqualTypeOf(); + + // Nested validator objects should have isOptional: "required" for fields with defaults + expect(_VNestedObject.fields.tags.isOptional).toBe("required"); + expect(_VNestedObject.fields.profile.fields.displayName.isOptional).toBe("required"); + expect(_VNestedObject.fields.profile.fields.isPublic.isOptional).toBe("required"); + expect(_VNestedObject.fields.id.isOptional).toBe("required"); +}); + test("zod output compliance", async () => { const t = convexTest(schema, modules); const response = await t.query(testApi.zodOutputCompliance, {}); diff --git a/packages/convex-helpers/server/zod3.ts b/packages/convex-helpers/server/zod3.ts index f9a53d82..17d0b642 100644 --- a/packages/convex-helpers/server/zod3.ts +++ b/packages/convex-helpers/server/zod3.ts @@ -599,6 +599,30 @@ type ConvexObjectValidatorFromZod = VObject< } >; +type ConvexObjectValidatorFromZodOutput = VObject< + ObjectType<{ + [key in keyof T]: T[key] extends z.ZodTypeAny + ? ConvexValidatorFromZodOutput + : never; + }>, + { + [key in keyof T]: ConvexValidatorFromZodOutput; + } +>; + +type ConvexUnionValidatorFromZodOutput = T extends z.ZodTypeAny[] + ? VUnion< + ConvexValidatorFromZodOutput["type"], + { + [Index in keyof T]: T[Index] extends z.ZodTypeAny + ? ConvexValidatorFromZodOutput + : never; + }, + "required", + ConvexValidatorFromZodOutput["fieldPaths"] + > + : never; + /** * Converts a Zod validator type * to the corresponding Convex validator type from `convex/values`. @@ -1040,9 +1064,9 @@ export type ConvexValidatorFromZodOutput = ConvexValidatorFromZodOutput > : Z extends z.ZodObject - ? ConvexObjectValidatorFromZod + ? ConvexObjectValidatorFromZodOutput : Z extends z.ZodUnion - ? ConvexUnionValidatorFromZod + ? ConvexUnionValidatorFromZodOutput : Z extends z.ZodDiscriminatedUnion ? VUnion< ConvexValidatorFromZodOutput["type"], @@ -1431,6 +1455,7 @@ export function zodOutputToConvexFields(zod: Z) { ) as { [k in keyof Z]: ConvexValidatorFromZodOutput }; } + interface ZidDef extends ZodTypeDef { typeName: "ConvexId"; tableName: TableName; From 397efaac084f85432368f46d00c421d8f3cdcd4d Mon Sep 17 00:00:00 2001 From: Nicolas Ettlin Date: Mon, 17 Nov 2025 10:25:07 -0800 Subject: [PATCH 2/2] Remove extraneous line return --- packages/convex-helpers/server/zod3.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/convex-helpers/server/zod3.ts b/packages/convex-helpers/server/zod3.ts index 17d0b642..79d43f49 100644 --- a/packages/convex-helpers/server/zod3.ts +++ b/packages/convex-helpers/server/zod3.ts @@ -1455,7 +1455,6 @@ export function zodOutputToConvexFields(zod: Z) { ) as { [k in keyof Z]: ConvexValidatorFromZodOutput }; } - interface ZidDef extends ZodTypeDef { typeName: "ConvexId"; tableName: TableName;