fix(open-api): correctly infer type for ZodDefault fields (#7150)

Co-authored-by: Taesu <bytaesu@gmail.com>
This commit is contained in:
Muzzaiyyan Hussain
2026-01-08 10:50:35 +09:00
committed by GitHub
co-authored by Taesu
parent 1e681542c3
commit d727c1b72f
3 changed files with 79 additions and 3 deletions
@@ -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",
],
},
@@ -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<any>) {
// 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>): 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>): 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;
@@ -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<string, any>;
// 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");
});
});