diff --git a/packages/better-auth/src/plugins/open-api/__snapshots__/open-api.test.ts.snap b/packages/better-auth/src/plugins/open-api/__snapshots__/open-api.test.ts.snap index a97e7d0a52..728d339023 100644 --- a/packages/better-auth/src/plugins/open-api/__snapshots__/open-api.test.ts.snap +++ b/packages/better-auth/src/plugins/open-api/__snapshots__/open-api.test.ts.snap @@ -3405,9 +3405,10 @@ exports[`open-api > should generate OpenAPI schema > openAPISchema 1`] = ` "type": "string", }, "rememberMe": { - "description": undefined, + "default": true, + "description": "If this is false, the session will not be remembered. Default is \`true\`.", "type": [ - "string", + "boolean", "null", ], }, diff --git a/packages/better-auth/src/plugins/open-api/generator.ts b/packages/better-auth/src/plugins/open-api/generator.ts index 6c4976152e..89bdc48be6 100644 --- a/packages/better-auth/src/plugins/open-api/generator.ts +++ b/packages/better-auth/src/plugins/open-api/generator.ts @@ -80,6 +80,10 @@ export interface Path { type AllowedType = "string" | "number" | "boolean" | "array" | "object"; const allowedType = new Set(["string", "number", "boolean", "array", "object"]); function getTypeFromZodType(zodType: z.ZodType) { + // unwrap ZodDefault to get the inner type + if (zodType instanceof z.ZodDefault) { + return getTypeFromZodType(zodType.unwrap() as any); + } const type = zodType.type; return allowedType.has(type) ? (type as AllowedType) : "string"; } @@ -192,7 +196,7 @@ function getRequestBody(options: EndpointOptions): any { function processZodType(zodType: z.ZodType): any { // optional unwrapping if (zodType instanceof z.ZodOptional) { - const innerType = (zodType as any)._def.innerType; + const innerType = zodType.unwrap() as any; const innerSchema = processZodType(innerType); if (innerSchema.type) { const type = Array.isArray(innerSchema.type) @@ -207,6 +211,20 @@ function processZodType(zodType: z.ZodType): any { anyOf: [innerSchema, { type: "null" }], }; } + // default unwrapping + if (zodType instanceof z.ZodDefault) { + const innerType = zodType.unwrap() as any; + const innerSchema = processZodType(innerType); + const defaultValueDef = (zodType as any)._def.defaultValue; + const defaultValue = + typeof defaultValueDef === "function" + ? defaultValueDef() + : defaultValueDef; + return { + ...innerSchema, + default: defaultValue, + }; + } // object unwrapping if (zodType instanceof z.ZodObject) { const shape = (zodType as any).shape; diff --git a/packages/better-auth/src/plugins/open-api/open-api.test.ts b/packages/better-auth/src/plugins/open-api/open-api.test.ts index 477ff53d25..936431d409 100644 --- a/packages/better-auth/src/plugins/open-api/open-api.test.ts +++ b/packages/better-auth/src/plugins/open-api/open-api.test.ts @@ -182,4 +182,61 @@ describe("open-api", async (it) => { checkNoNullable(requestBodySchema, "signInSocialRequestBody"), ).not.toThrow(); }); + + it("should correctly unwrap ZodDefault and infer inner type", async () => { + const schema = await auth.api.generateOpenAPISchema(); + const paths = schema.paths as Record; + + // Helper to extract base type from type array + const getBaseType = (type: any) => { + if (Array.isArray(type)) { + return type.filter((t) => t !== "null"); + } + return [type]; + }; + + // Helper to check if field has correct type and default value + const validateDefaultField = ( + properties: any, + fieldName: string, + expectedType: string, + expectedDefault?: any, + ) => { + expect(properties[fieldName]).toBeDefined(); + const fieldSchema = properties[fieldName]; + + // Check type is correctly inferred - not fallback to "string" + const baseTypes = getBaseType(fieldSchema.type); + expect(baseTypes).toContain(expectedType); + expect(baseTypes).not.toContain("string"); + + // Check default value is included if expected + if (expectedDefault !== undefined) { + expect(fieldSchema.default).toBe(expectedDefault); + } + }; + + // Test sign-in endpoint: z.boolean().default(true).optional() + const signInPath = paths["/sign-in/email"]; + expect(signInPath).toBeDefined(); + expect(signInPath.post).toBeDefined(); + expect(signInPath.post.requestBody).toBeDefined(); + + const signInProps = + signInPath.post.requestBody.content["application/json"].schema.properties; + validateDefaultField(signInProps, "rememberMe", "boolean", true); + + // Test sign-up endpoint: z.boolean().optional() - no default + const signUpPath = paths["/sign-up/email"]; + expect(signUpPath).toBeDefined(); + expect(signUpPath.post).toBeDefined(); + expect(signUpPath.post.requestBody).toBeDefined(); + + const signUpProps = + signUpPath.post.requestBody.content["application/json"].schema.properties; + // Should still be boolean, just without default + expect(signUpProps.rememberMe).toBeDefined(); + const baseTypes = getBaseType(signUpProps.rememberMe.type); + expect(baseTypes).toContain("boolean"); + }); });