mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-20 20:26:55 -05:00
fix(open-api): correctly infer type for ZodDefault fields (#7150)
Co-authored-by: Taesu <bytaesu@gmail.com>
This commit is contained in:
co-authored by
Taesu
parent
1e681542c3
commit
d727c1b72f
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user