Skip to content

fix(openapi-server): handle explode:true array query params in hono/express routers - #407

Merged
benjamineckstein merged 1 commit into
mainfrom
fix/377-hono-express-array-query-params
Jul 4, 2026
Merged

fix(openapi-server): handle explode:true array query params in hono/express routers#407
benjamineckstein merged 1 commit into
mainfrom
fix/377-hono-express-array-query-params

Conversation

@benjamineckstein

@benjamineckstein benjamineckstein commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #377.

The hono and express router emitters had no explode:true array branch for query params, so a type: array query param fell through to z.string() in the generated Zod schema and was extracted as a single scalar, while the generated service method expects an array (number[] / boolean[] / string[]). Result: a TS2322 type mismatch in the generated output. #375 fixed only the Fastify target and left hono/express as a known gap (tracked here).

The QueryParam type already carried isArray / itemsTsType (the service side was already correct at number[]); the router emitters just weren't consuming them.

Fix (router.ts)

Added an isArray branch to queryParamZodExpr (mirrors Fastify's queryParamBaseExpr ordering) plus a queryParamItemZodExpr helper, and taught both extraction emitters to collect repeated keys:

Honoc.req.queries("name") returns all values for a key:

ids:    c.req.queries("ids")?.map(Number),
active: c.req.queries("active")?.map((v) => v === "true"),
// schema: z.array(z.coerce.number()).optional() / z.array(z.boolean()).optional()

Express — qs yields string[] for a repeated key but a bare string for a single occurrence, so a small emitted normalizer arrays it first:

function _toQueryArray(v) { return v === undefined ? undefined : Array.isArray(v) ? v : [v] }
ids: _toQueryArray(req.query["ids"])?.map(Number),

Numbers are coerced in extraction and validated with z.coerce.number() (harmless no-op, matches Fastify); booleans are coerced in extraction (=== 'true') and validated with z.boolean(). Delimited (explode:false) and deepObject paths are untouched (mutually exclusive with isArray).

Typechecked guard

The issue's root cause was that compat-matrix.test.ts only asserts "generates without throwing", not that output typechecks. So this PR adds a guard that runs generated service.ts + router.ts through the real TypeScript compiler (ts.createProgram) for hono, express and fastify. Proven non-vacuous: stashing the fix reproduces exactly the TS2322 from the bug report for both hono and express.

Notes for reviewers

  • devDeps: hono, express, @types/express, zod added to openapi-server devDependencies (and to .fallowrc.json ignoreDependencies, same bucket as the existing fastify-type-provider-zod) purely so the typecheck guard can resolve real framework type declarations on disk. They are not runtime deps and are only referenced inside generated string templates.
  • shared.ts: the only intended change is a comment update (the old comment claimed hono/express don't handle array query params, now false). The surrounding getBodyInfo reformatting is the repo's pre-commit Prettier hook normalizing pre-existing lines, not a logic change.
  • Scoped the guard to a self-contained compiler test rather than extending examples/ or the shared petstore spec, to avoid a wide blast radius across the e2e/contract packages.

Verification

  • pnpm --filter @codewithagents/openapi-server test → 16 files, 644 passed
  • test:matrix (128-spec compat) → 384 passed, no regressions
  • lint (tsc --noEmit) → clean
  • test:coverage → 92.4 / 86.9 / 96.7 / 94.8, above the 85/75/88/85 floors
  • pnpm fallow:audit → clean for the changed files

Summary by CodeRabbit

  • New Features

    • Added support for repeated-key array query parameters in generated Hono and Express code.
    • Array items now preserve correct types such as strings, numbers, and booleans.
  • Bug Fixes

    • Improved handling of explode: true query arrays so they no longer fall back to scalar parsing.
    • Fixed TypeScript compatibility checks for generated code involving array query parameters.
  • Chores

    • Updated local ignore and development configuration.

…xpress routers

The hono and express router emitters had no branch for explode:true array
query params (type: array with the default explode). Such a param fell
through to z.string() in the emitted Zod schema, and extraction read a
single scalar value, while the generated service method expects an array
type (number[] / boolean[] / string[]). This produced a TS2322 type
mismatch in the generated output. The Fastify emitter already handled this
correctly (#375/#348); this closes the same gap for hono and express.

router.ts changes:
- queryParamZodExpr: new isArray branch emits z.array(<itemExpr>), mirroring
  the Fastify emitter's item coercion (number -> z.coerce.number(), boolean
  -> z.boolean(), else z.string()).
- Hono extraction: c.req.queries(name) collects every repeated key into
  string[] | undefined, then items are coerced (.map(Number) / .map(v =>
  v === 'true')) to match the Zod validation.
- Express extraction: qs normalizes a repeated key to string[], but a
  single occurrence stays a bare string and an absent key is undefined.
  A small shared _toQueryArray(...) helper (emitted once per file, only
  when needed) normalizes all three cases before the same item coercion.

Adds a typechecked guard: compat-matrix.test.ts only asserts generation
"does not throw", never that the output actually compiles, which is why
this slipped through. array-query-typecheck.test.ts feeds the real
generated service.ts + router.ts through the TypeScript compiler API for
an explode:true integer array query param, across hono, express, and
fastify (as a control), plus a sanity check proving the harness itself
catches a genuine type mismatch. Requires real zod/hono/express/fastify
type declarations on disk, so those are added as devDependencies here.

hono-express-array-query.test.ts adds string-level regression tests for
the emitted Zod expressions and extraction snippets across integer/
boolean/string array items and required/optional variants.

Closes #377
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR fixes explode:true array query parameter handling in generated Hono and Express routers, adding array-aware Zod schema generation and repeated-key extraction to match the existing Fastify behavior. It includes new TypeScript compile-based regression tests, test compilation helpers, formatting cleanups in shared.ts, and updated dev dependencies/config for test tooling.

Changes

Array Query Param Fix

Layer / File(s) Summary
Tooling and dependency setup
.gitignore, .fallowrc.json, packages/openapi-server/package.json
Ignores .typecheck-scratch/, adds hono/express/@types/express to ignoreDependencies, and adds express, hono, zod, @types/express as devDependencies.
Router query param array Zod generation and extraction
packages/openapi-server/src/plugins/router.ts
Adds item/array Zod helpers, routes isArray params through the new array builder, adds honoArrayQueryExpr/expressArrayQueryExpr extraction, wires them into route field assembly, and conditionally emits an Express _toQueryArray normalization helper.
Shared query array documentation and formatting
packages/openapi-server/src/plugins/shared.ts
Updates array-style documentation comment to reflect cross-framework support and reformats several BodyInfo return literals and a function signature without behavior changes.
TypeScript compile-check test utilities
packages/openapi-server/src/__tests__/ts-compile-helpers.ts
Adds compileGeneratedFiles and assertNoTsDiagnostics helpers that compile generated source in a temp scratch directory and assert no TypeScript diagnostics.
Regression tests for array query params
packages/openapi-server/src/__tests__/array-query-typecheck.test.ts, packages/openapi-server/src/__tests__/hono-express-array-query.test.ts
Adds typecheck and unit tests validating generated Zod array schemas, repeated-key extraction, required-array handling, and unaffected explode:false behavior across Hono, Express, and Fastify.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant HonoRouter
  participant ExpressRouter
  participant ZodSchema
  participant Service

  Client->>HonoRouter: GET /items?ids=1&ids=2
  HonoRouter->>HonoRouter: honoArrayQueryExpr uses c.req.queries("ids")
  HonoRouter->>ZodSchema: validate z.array(itemExpr)
  ZodSchema-->>HonoRouter: number[] ids
  HonoRouter->>Service: listItems({ ids })

  Client->>ExpressRouter: GET /items?ids=1&ids=2
  ExpressRouter->>ExpressRouter: expressArrayQueryExpr calls _toQueryArray(req.query["ids"])
  ExpressRouter->>ZodSchema: validate z.array(itemExpr)
  ZodSchema-->>ExpressRouter: number[] ids
  ExpressRouter->>Service: listItems({ ids })
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: fixing explode:true array query params in hono/express routers.
Linked Issues check ✅ Passed The PR adds array-aware Zod generation, repeated-key extraction, and end-to-end typecheck tests for hono/express as requested.
Out of Scope Changes check ✅ Passed The extra dependency, test, and ignore-file updates support the typecheck fix and do not appear unrelated to the issue.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/377-hono-express-array-query-params

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Fallow audit report

Found 3 findings.

Details
Severity Rule Location Description
minor fallow/unused-dev-dependency examples/package.json:15 Package '@tanstack/react-query' is in devDependencies but never imported; imported in other workspaces: packages/integration, packages/petstore-fastify
minor fallow/unused-dev-dependency examples/package.json:17 Package 'react' is in devDependencies but never imported; imported in other workspaces: packages/integration, packages/petstore-fastify
minor fallow/unused-dev-dependency packages/integration/package.json:24 Package 'fastify' is in devDependencies but never imported; imported in other workspaces: packages/petstore-contract, packages/petstore-fastify

Generated by fallow.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Fallow audit report

0 inline findings selected for GitHub review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/openapi-server/src/plugins/router.ts (1)

310-336: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor duplication between Hono/Express array extraction builders.

honoArrayQueryExpr and expressArrayQueryExpr share identical item-coercion suffix logic (?.map(Number) / ?.map((v) => v === 'true')), differing only in the base expression. Could be factored into a shared helper taking the base expression string, reducing duplication if a third item type is added later.

♻️ Optional refactor: extract shared coercion suffix
+function withArrayItemCoercion(base: string, itemsTsType: string | undefined): string {
+  if (itemsTsType === 'number') return `${base}?.map(Number)`
+  if (itemsTsType === 'boolean') return `${base}?.map((v) => v === 'true')`
+  return base
+}
+
 function honoArrayQueryExpr(q: QueryParam): string {
-  const base = `c.req.queries('${q.rawName}')`
-  if (q.itemsTsType === 'number') return `${base}?.map(Number)`
-  if (q.itemsTsType === 'boolean') return `${base}?.map((v) => v === 'true')`
-  return base
+  return withArrayItemCoercion(`c.req.queries('${q.rawName}')`, q.itemsTsType)
 }

 function expressArrayQueryExpr(q: QueryParam): string {
-  const base = `_toQueryArray(req.query['${q.rawName}'] as string | string[] | undefined)`
-  if (q.itemsTsType === 'number') return `${base}?.map(Number)`
-  if (q.itemsTsType === 'boolean') return `${base}?.map((v) => v === 'true')`
-  return base
+  return withArrayItemCoercion(
+    `_toQueryArray(req.query['${q.rawName}'] as string | string[] | undefined)`,
+    q.itemsTsType
+  )
 }

Correctness-wise this segment is solid: boolean items are pre-coerced to real booleans (matching z.boolean(), not .coerce), and number items pre-coerced via Number() combined with z.coerce.number() on the Zod side correctly rejects invalid input (Zod rejects NaN for z.number()/z.coerce.number()).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/openapi-server/src/plugins/router.ts` around lines 310 - 336, The
Hono and Express array query extraction helpers duplicate the same item-coercion
logic, so factor the shared `?.map(Number)` / `?.map((v) => v === 'true')`
suffix into a common helper and keep `honoArrayQueryExpr` and
`expressArrayQueryExpr` focused on only building their base query expression.
Use the existing symbols `honoArrayQueryExpr`, `expressArrayQueryExpr`, and
`QueryParam` to introduce a small shared helper that takes the base expression
string plus `itemsTsType`, then have both functions delegate to it to reduce
duplication and make future item-type additions easier.
packages/openapi-server/src/__tests__/array-query-typecheck.test.ts (1)

48-80: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider parameterizing the three near-identical it blocks.

Hono/Express/Fastify cases repeat the same generate → compile → assert pattern with only the generator functions differing. A small it.each table would reduce duplication, though the current form is readable and low-risk.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/openapi-server/src/__tests__/array-query-typecheck.test.ts` around
lines 48 - 80, The three test cases in array-query-typecheck.test.ts duplicate
the same generate-compile-assert flow for Hono, Express, and Fastify. Refactor
the repeated `it` blocks into a parameterized table-driven test (for example,
using a single shared helper or `it.each`) that calls the appropriate generators
like `generateService`, `generateRouter`, `generateExpressRouter`,
`generateFastifyTypedService`, and `generateFastifyRouter`, while preserving the
existing compile and `assertNoTsDiagnostics` checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/openapi-server/src/__tests__/ts-compile-helpers.ts`:
- Around line 42-53: The TypeScript compiler options parsing in
ts-compile-helpers is ignoring the errors returned by
ts.convertCompilerOptionsFromJson, so invalid hardcoded options can be silently
accepted. Update the helper that builds compiler options to capture both options
and errors, and make it fail or report when errors are present instead of
proceeding with only options. Keep the change localized to the
ts.convertCompilerOptionsFromJson call site so any bad config in the test helper
surfaces immediately.

---

Nitpick comments:
In `@packages/openapi-server/src/__tests__/array-query-typecheck.test.ts`:
- Around line 48-80: The three test cases in array-query-typecheck.test.ts
duplicate the same generate-compile-assert flow for Hono, Express, and Fastify.
Refactor the repeated `it` blocks into a parameterized table-driven test (for
example, using a single shared helper or `it.each`) that calls the appropriate
generators like `generateService`, `generateRouter`, `generateExpressRouter`,
`generateFastifyTypedService`, and `generateFastifyRouter`, while preserving the
existing compile and `assertNoTsDiagnostics` checks.

In `@packages/openapi-server/src/plugins/router.ts`:
- Around line 310-336: The Hono and Express array query extraction helpers
duplicate the same item-coercion logic, so factor the shared `?.map(Number)` /
`?.map((v) => v === 'true')` suffix into a common helper and keep
`honoArrayQueryExpr` and `expressArrayQueryExpr` focused on only building their
base query expression. Use the existing symbols `honoArrayQueryExpr`,
`expressArrayQueryExpr`, and `QueryParam` to introduce a small shared helper
that takes the base expression string plus `itemsTsType`, then have both
functions delegate to it to reduce duplication and make future item-type
additions easier.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 997460c9-0840-4cd5-abbc-06e84b73b2d2

📥 Commits

Reviewing files that changed from the base of the PR and between fb3d7b0 and 31cfe68.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • .fallowrc.json
  • .gitignore
  • packages/openapi-server/package.json
  • packages/openapi-server/src/__tests__/array-query-typecheck.test.ts
  • packages/openapi-server/src/__tests__/hono-express-array-query.test.ts
  • packages/openapi-server/src/__tests__/ts-compile-helpers.ts
  • packages/openapi-server/src/plugins/router.ts
  • packages/openapi-server/src/plugins/shared.ts

Comment on lines +42 to +53
const { options } = ts.convertCompilerOptionsFromJson(
{
strict: true,
target: 'ES2022',
module: 'ESNext',
moduleResolution: 'Bundler',
noEmit: true,
skipLibCheck: true,
lib: ['ES2022', 'DOM'],
},
dir
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Compiler-options parsing errors are silently discarded.

ts.convertCompilerOptionsFromJson returns { options, errors }; only options is destructured, so a typo or invalid option value in the hardcoded config would be swallowed instead of surfacing, potentially causing the compile to silently run with unintended defaults.

🛡️ Proposed fix
-    const { options } = ts.convertCompilerOptionsFromJson(
+    const { options, errors } = ts.convertCompilerOptionsFromJson(
       {
         strict: true,
         target: 'ES2022',
         module: 'ESNext',
         moduleResolution: 'Bundler',
         noEmit: true,
         skipLibCheck: true,
         lib: ['ES2022', 'DOM'],
       },
       dir
     )
+    if (errors.length > 0) {
+      throw new Error(`Invalid compiler options: ${errors.map((e) => e.messageText).join('\n')}`)
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { options } = ts.convertCompilerOptionsFromJson(
{
strict: true,
target: 'ES2022',
module: 'ESNext',
moduleResolution: 'Bundler',
noEmit: true,
skipLibCheck: true,
lib: ['ES2022', 'DOM'],
},
dir
)
const { options, errors } = ts.convertCompilerOptionsFromJson(
{
strict: true,
target: 'ES2022',
module: 'ESNext',
moduleResolution: 'Bundler',
noEmit: true,
skipLibCheck: true,
lib: ['ES2022', 'DOM'],
},
dir
)
if (errors.length > 0) {
throw new Error(`Invalid compiler options: ${errors.map((e) => e.messageText).join('\n')}`)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/openapi-server/src/__tests__/ts-compile-helpers.ts` around lines 42
- 53, The TypeScript compiler options parsing in ts-compile-helpers is ignoring
the errors returned by ts.convertCompilerOptionsFromJson, so invalid hardcoded
options can be silently accepted. Update the helper that builds compiler options
to capture both options and errors, and make it fail or report when errors are
present instead of proceeding with only options. Keep the change localized to
the ts.convertCompilerOptionsFromJson call site so any bad config in the test
helper surfaces immediately.

@benjamineckstein
benjamineckstein merged commit a4805b7 into main Jul 4, 2026
18 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 4, 2026
@benjamineckstein
benjamineckstein deleted the fix/377-hono-express-array-query-params branch July 4, 2026 10:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

openapi-server: hono/express routers do not handle explode:true array query params (scalar extraction -> type mismatch)

1 participant