chanfana
Version:
OpenAPI 3 and 3.1 schema generator and validator for Hono, itty-router and more!
102 lines (93 loc) • 3.71 kB
text/typescript
import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";
import { z } from "zod";
import type { AnyZodObject, RouteParameter } from "./types";
extendZodWithOpenApi(z);
/**
* Helper function to unwrap optional/nullable types and check instanceof.
* Handles Zod's wrapper types like ZodOptional, ZodNullable, ZodDefault.
*
* @param schema - The Zod schema to check
* @param ZodClass - The Zod class to check against
* @returns True if the unwrapped schema is an instance of ZodClass
*/
function unwrapAndCheck(schema: any, ZodClass: any): boolean {
let current = schema;
// Check first before unwrapping (important for arrays which also have .unwrap())
if (current instanceof ZodClass) {
return true;
}
// Unwrap optional/nullable/default wrappers
while (current && typeof current.unwrap === "function") {
current = current.unwrap();
if (current instanceof ZodClass) {
return true;
}
}
return false;
}
/**
* Coerces input data to match expected Zod schema types.
* Handles query strings, path params, headers, and cookies.
*
* Transformations:
* - Empty strings → null
* - Duplicate keys → arrays
* - String "true"/"false" → boolean (for ZodBoolean)
* - Numeric strings → numbers (for ZodNumber)
* - Numeric strings → BigInt (for ZodBigInt)
* - Date strings → Date objects (for ZodDate)
*
* @param data - The input data (URLSearchParams or plain object)
* @param schema - Optional Zod schema to guide coercion
* @returns Coerced data object or null if empty
*/
export function coerceInputs(data: Record<string, any>, schema?: RouteParameter): Record<string, any> | null {
// For older node versions, searchParams is just an object without the size property
if (data.size === 0 || (data.size === undefined && typeof data === "object" && Object.keys(data).length === 0)) {
return null;
}
const params: Record<string, any> = {};
const entries = data.entries ? data.entries() : Object.entries(data);
for (let [key, value] of entries) {
// Query, path and headers can be empty strings, that should equal to null as nothing was provided
if (value === "") {
value = null;
}
if (params[key] === undefined) {
params[key] = value;
} else if (!Array.isArray(params[key])) {
params[key] = [params[key], value];
} else {
params[key].push(value);
}
let innerType;
if (schema && (schema as AnyZodObject).shape && (schema as AnyZodObject).shape[key]) {
innerType = (schema as AnyZodObject).shape[key];
} else if (schema) {
// Fallback for Zod effects
innerType = schema;
}
// Soft transform query strings into expected types
if (innerType && params[key] !== null) {
if (unwrapAndCheck(innerType, z.ZodArray) && !Array.isArray(params[key])) {
params[key] = [params[key]];
} else if (unwrapAndCheck(innerType, z.ZodBoolean) && typeof params[key] === "string") {
const _val = params[key].toLowerCase().trim();
if (_val === "true" || _val === "false") {
params[key] = _val === "true";
}
} else if (unwrapAndCheck(innerType, z.ZodNumber) && typeof params[key] === "string") {
params[key] = Number.parseFloat(params[key]);
} else if (unwrapAndCheck(innerType, z.ZodBigInt) && typeof params[key] === "string") {
try {
params[key] = BigInt(params[key]);
} catch {
// If BigInt conversion fails, leave as string for Zod to handle validation
}
} else if (unwrapAndCheck(innerType, z.ZodDate) && typeof params[key] === "string") {
params[key] = new Date(params[key]);
}
}
}
return params;
}