From 13a4b4173f1ef2773a3d7317962961c347796123 Mon Sep 17 00:00:00 2001 From: Max Holman Date: Thu, 9 Apr 2026 15:52:48 +0700 Subject: [PATCH] fix: resolve $ref schemas for query and header param coercion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a query or header param schema is a $ref to a component schema (e.g., ExpireTime with type: "integer"), the ref was passed through unchanged to the valibot generator which emitted v.number() — failing on string values from HTTP. Now $ref schemas are resolved inline for query/header params so the string-to-number coercion pipeline applies. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../__snapshots__/nullables.test.ts.snap | 2 ++ __tests__/nullables.test.ts | 9 ++++-- lib/process-document.ts | 30 +++++++++++++++---- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/__tests__/__snapshots__/nullables.test.ts.snap b/__tests__/__snapshots__/nullables.test.ts.snap index 1f9427d..77e58ed 100644 --- a/__tests__/__snapshots__/nullables.test.ts.snap +++ b/__tests__/__snapshots__/nullables.test.ts.snap @@ -77,6 +77,7 @@ exports[`nullables 1`] = ` exports[`query and header integer params coerce strings to numbers 1`] = ` "export type Dummy = string; +export type ExpireTime = number; export type ListFilesCommandQuery = { exp: \`\${number}\`; limit?: \`\${number}\` | undefined; @@ -92,6 +93,7 @@ exports[`query and header integer params coerce strings to numbers 2`] = ` "import * as v from "valibot"; export const dummySchema = v.string(); +export const expireTimeSchema = v.pipe(v.number(), v.integer(), v.minValue(0)); export const listFilesCommandQuerySchema = v.strictObject({ "exp": v.pipe(v.string(), v.digits(), v.transform((n) => Number.parseInt(n, 10)), v.number(), v.integer(), v.minValue(0)), "limit": v.exactOptional(v.pipe(v.string(), v.digits(), v.transform((n) => Number.parseInt(n, 10)), v.number(), v.integer(), v.minValue(1), v.maxValue(100))) diff --git a/__tests__/nullables.test.ts b/__tests__/nullables.test.ts index 8849ece..ca16aef 100644 --- a/__tests__/nullables.test.ts +++ b/__tests__/nullables.test.ts @@ -113,6 +113,11 @@ test("query and header integer params coerce strings to numbers", async () => { components: { schemas: { Dummy: { type: "string" }, + ExpireTime: { + type: "integer", + format: "int64", + minimum: 0, + }, }, }, paths: { @@ -125,9 +130,7 @@ test("query and header integer params coerce strings to numbers", async () => { in: "query", required: true, schema: { - type: "integer", - format: "int64", - minimum: 0, + $ref: "#/components/schemas/ExpireTime", }, }, { diff --git a/lib/process-document.ts b/lib/process-document.ts index 4e028ae..9d2b19f 100644 --- a/lib/process-document.ts +++ b/lib/process-document.ts @@ -407,12 +407,32 @@ export async function processOpenApiDocument( // }); } - if (resolvedParameter.in === "query") { - queryParameters.push(resolvedParameter); - } + if ( + resolvedParameter.in === "query" || + resolvedParameter.in === "header" + ) { + // Resolve $ref schemas so the valibot coercion + // pipeline can inspect the underlying type + const resolvedSchema = + resolvedParameter.schema && + "$ref" in resolvedParameter.schema + ? (refs.get(resolvedParameter.schema.$ref) ?? undefined) + : undefined; + + const paramWithResolvedSchema: oas30.ParameterObject = { + ...resolvedParameter, + ...(resolvedSchema && + typeof resolvedSchema === "object" && + !Array.isArray(resolvedSchema) && { + schema: resolvedSchema, + }), + }; - if (resolvedParameter.in === "header") { - headerParameters.push(resolvedParameter); + if (resolvedParameter.in === "query") { + queryParameters.push(paramWithResolvedSchema); + } else { + headerParameters.push(paramWithResolvedSchema); + } } }