Skip to content

feat(typescript): add queryBuilder pattern for query parameter serialization#15026

Open
Swimburger wants to merge 30 commits intomainfrom
devin/1776263972-add-comma-array-format
Open

feat(typescript): add queryBuilder pattern for query parameter serialization#15026
Swimburger wants to merge 30 commits intomainfrom
devin/1776263972-add-comma-array-format

Conversation

@Swimburger
Copy link
Copy Markdown
Member

@Swimburger Swimburger commented Apr 15, 2026

Description

Refactors TypeScript query parameter serialization to use a fluent queryBuilder() pattern, matching the C# generator architecture. The generator now emits a chained builder (core.url.queryBuilder().addMany(…).add(…, { style: "comma" }).build()) so each parameter's serialization format (repeat vs comma) is decided at code-gen time rather than runtime via a config map.

OpenAPI query parameters with explode: false and style: form use .add("tags", tags, { style: "comma" }) to produce comma-separated values (e.g., tags=a,b,c), while all other array parameters use the default repeat format (tags=a&tags=b&tags=c).

Each .add() call eagerly serializes the parameter immediately (matching the C# QueryStringBuilder pattern), storing the result in a Map<string, string> — no separate format tracking map is needed. Non-comma params are added in bulk via .addMany(), then comma-style params override their keys individually.

Changes Made

Runtime (core-utilities)

  • New: src/core/url/QueryStringBuilder.ts — Exports a queryBuilder() factory function (class is private). Provides three fluent methods:
    • .add(key, value, options?) — eagerly serializes a single parameter via toQueryString(), with optional { style: "comma" } for comma-separated arrays
    • .addMany(params) — batch-adds all entries from a Record<string, unknown> using the default repeat format; null/undefined values are silently skipped
    • .mergeAdditional(params?) — overrides existing keys with runtime requestOptions.queryParams (last-write-wins via Map.set)
    • .build() — joins pre-serialized parts with &
  • src/core/url/index.ts — Re-exports queryBuilder.
  • src/core/url/qs.ts — Added "comma" to the ArrayFormat type. Comma format encodes each value individually before joining with literal commas (so commas within values become %2C while separator commas remain literal). Null/undefined values within arrays are skipped entirely (matching qs npm library behavior). The API matches the qs npm library — no custom extensions.
  • src/core/fetcher/Fetcher.ts — Added queryString?: string to Fetcher.Args. When provided, the fetcher uses the pre-built string instead of calling createRequestUrl for query params.
  • src/core/fetcher/createRequestUrl.ts — Removed the old arrayFormats third parameter; the function now only takes baseUrl and queryParameters.

Generator (commons + client-class-generator)

  • commons/src/core-utilities/UrlUtils.ts — Exposes queryBuilder._invoke() so generated code can call the factory function.
  • commons/src/core-utilities/Fetcher.ts — Added queryString property to generated Fetcher.Args interface and _invoke emission.
  • endpoints/utils/GeneratedQueryParams.ts — New getQueryStringExpression(context) method emits the builder chain. Separates params into non-comma (batched via .addMany(_queryParams)) and comma (individual .add(key, value, { style: "comma" }) overrides). Uses explode === false to select comma style.
  • endpoint-request/Generated{Default,Bytes,FileUpload}EndpointRequest.ts — Updated getFetcherRequestArgs() return types and implementations to include queryString from the builder.
  • endpoints/default/GeneratedDefaultEndpointImplementation.tsbuildFetcherArgs() now passes queryString when present.

Wire test example generation (IR pipeline)

  • packages/cli/generation/ir-generator/src/converters/services/convertExampleEndpointCall.ts — When converting Fern definition examples for allow-multiple query params with array values, each element is now converted individually and wrapped in a list container (ExampleContainer.list).
  • test-definitions/fern/apis/query-parameters-openapi/openapi.yml — Added example values to tags (["ACCESS_GRANTED", "COPY"]) and optionalTags (["DELETE", "MOVE"]) parameters so wire tests exercise the comma format with real array data.

Snapshot updates

  • IR generator test definitions — Regenerated 7 JSON files in packages/cli/generation/ir-generator-tests/.
  • IR migration snapshots — Updated v57-to-v56 and v58-to-v57 migration snapshots.
  • ETE test snapshots — Updated dynamic.test.ts.snap, ir.test.ts.snap, validate.test.ts.snap, diff.test.ts.snap, and dynamic.json.

Seed output

  • Regenerated all affected ts-sdk seed fixtures. Generated code now uses the addMany + comma override pattern:
    queryString: core.url
        .queryBuilder()
        .addMany(_queryParams)
        .add("tags", _queryParams.tags, { style: "comma" })
        .mergeAdditional(requestOptions?.queryParams)
        .build()

Changelog

  • Added unreleased feat entry under generators/typescript/sdk/changes/unreleased/.
  • Added unreleased fix entry under packages/cli/cli/changes/unreleased/ for array example support.

Human Review Checklist

  • addMany + comma override pattern: Comma-style params are added twice — first via .addMany(_queryParams) (repeat format) then overridden via .add(key, value, { style: "comma" }). This relies on Map.set last-write-wins. Verify this is acceptable vs. filtering comma params out of the addMany call.
  • Builder emitted for all endpoints with query params: Every endpoint that has query parameters gets a full builder chain, even if none use explode: false. Confirm this is acceptable vs. only emitting the builder when at least one comma param exists.
  • Redundant queryParameters computation: Generated code still computes the queryParameters object and the queryString builder chain. When queryString is present, it takes precedence in the fetcher, making the object value unused at runtime. Verify this dual-emit is acceptable or if the legacy queryParameters path should be removed.
  • mergeAdditional override semantics: When requestOptions.queryParams contains a key that was already .add()-ed, mergeAdditional uses Map.set (last-write-wins). Confirm this is the desired precedence.
  • Comma encoding divergence from qs library: The comma format encodes each value then joins with literal commas, producing a%2Cb,c for ["a,b", "c"]. The real qs library encodes commas too (a%2Cb%2Cc). This divergence is intentional (unambiguous output), but worth confirming.
  • Null/undefined skip in comma format: Null/undefined array elements are skipped entirely (["a", null, "c"]a,c), matching qs library behavior. Verify this is the desired behavior.

Testing

  • Unit tests added for QueryStringBuilder (34 tests: .add() repeat + comma styles, .addMany() batch, chaining, .mergeAdditional(), edge cases, end-to-end scenario)
  • Unit tests added for comma array format in qs.ts (12 tests)
  • Existing qs.ts and createRequestUrl.ts tests passing
  • Wire tests exercise builder chain with { style: "comma" } for explode: false params
  • All 207 ts-sdk seed fixtures regenerated and passing
  • IR generator test definitions regenerated (7 JSON files)
  • IR migration snapshots updated (v57-to-v56, v58-to-v57)
  • ETE test snapshots updated (dynamic, IR, validate, diff)
  • Generator packages compile successfully

Link to Devin session: https://app.devin.ai/sessions/5b96d911886f499cbfabba83155f56a6
Requested by: @Swimburger


Open with Devin

…alizer

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@github-actions
Copy link
Copy Markdown
Contributor

🌱 Seed Test Selector

Select languages to run seed tests for:

  • Python
  • TypeScript
  • Java
  • Go
  • Ruby
  • C#
  • PHP
  • Swift
  • Rust
  • OpenAPI

How to use: Click the ⋯ menu above → "Edit" → check the boxes you want → click "Update comment". Tests will run automatically and snapshots will be committed to this PR.

hex-security-app[bot]

This comment was marked as resolved.

…rayFormat through Fetcher.Args

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 15, 2026

SDK Generation Benchmark Results

Comparing PR branch against latest nightly baseline on main (2026-04-15T21:03:37Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square 96s 146s 96s +0s (+0.0%)
go-sdk square 113s 145s 110s -3s (-2.7%)
java-sdk square 184s 188s 176s -8s (-4.3%)
php-sdk square 90s 128s 86s -4s (-4.4%)
python-sdk square 115s 155s 118s +3s (+2.6%)
ruby-sdk-v2 square 118s 142s 118s +0s (+0.0%)
rust-sdk square 96s 96s 97s +1s (+1.0%)
swift-sdk square 89s 132s 85s -4s (-4.5%)
ts-sdk square 103s 140s 99s -4s (-3.9%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-04-15T21:03:37Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-04-16 05:00 UTC

Swimburger and others added 4 commits April 15, 2026 15:00
…pport

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…lse params

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…p instead of single value

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration devin-ai-integration bot changed the title feat(typescript): add comma array format support to query string serializer feat(typescript): add per-parameter comma array format for explode:false query params Apr 15, 2026
Swimburger and others added 2 commits April 15, 2026 15:33
…n type in query param array formats

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ter serialization

Replace per-parameter config map (queryParameterArrayFormats) with a fluent
QueryStringBuilder that matches the C# architecture. The generator now emits
chained .add() / .addComma() calls so serialization format is decided at
code-gen time rather than runtime.

- Add QueryStringBuilder class with .add(), .addComma(), .mergeAdditional(), .build()
- Update Fetcher.Args to accept pre-built queryString instead of queryParameterArrayFormats
- Update all endpoint implementations to use builder pattern via getQueryStringExpression()
- Remove getQueryParameterArrayFormats helper functions from endpoint implementations
- Regenerate all ts-sdk seed output

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration devin-ai-integration bot changed the title feat(typescript): add per-parameter comma array format for explode:false query params feat(typescript): use QueryStringBuilder pattern for query parameter serialization Apr 15, 2026
Swimburger and others added 2 commits April 15, 2026 16:44
…ral comma separators

The addComma() method correctly produces literal comma separators (tags=a,b,c)
for OpenAPI explode:false parameters. The test expectations were incorrectly
expecting %2C-encoded separators. Fixed all 4 affected test assertions.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…new QueryStringBuilder()

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration devin-ai-integration bot changed the title feat(typescript): use QueryStringBuilder pattern for query parameter serialization feat(typescript): add queryBuilder pattern for query parameter serialization Apr 15, 2026
Swimburger and others added 6 commits April 15, 2026 17:35
…minate arrayFormats tracking

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…e: "comma" } API

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…Url (now handled by queryBuilder)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… now matches qs library

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ests

- Modify V1 auto-example generator to wrap scalar examples in list containers
  when query parameter has allowMultiple=true, so wire tests exercise the
  array serialization path (comma format)
- Modify convertExampleEndpointCall to handle array examples for
  allow-multiple query params from Fern definition examples
- Add example values to query-parameters-openapi test fixture
- Regenerate all affected ts-sdk seed fixtures

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 15, 2026

Docs Generation Benchmark Results

Comparing PR branch against latest nightly baseline on main (2026-04-15T21:03:37Z).

Fixture main PR Delta
docs 319.8s 291.6s (35 versions) -28.2s (-8.8%)

Docs generation runs fern generate --docs --preview end-to-end against the benchmark fixture with 35 API versions (each version: markdown processing + OpenAPI-to-IR + FDR upload).
Delta is computed against the nightly baseline on main.
Baseline from nightly run(s) on main (latest: 2026-04-15T21:03:37Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-04-16 05:01 UTC

Swimburger and others added 3 commits April 15, 2026 20:38
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ples

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Swimburger and others added 2 commits April 15, 2026 22:12
…ll snapshots

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
description: "should handle undefined values in comma format",
input: { items: ["a", undefined, "c"] },
options: { arrayFormat: "comma" },
expected: "items=a,,c",
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

does this match qs behavior?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Already fixed in commit edfea2cdc7e — the tests now expect skip behavior matching qs:

{
    description: "should skip null values in comma format",
    input: { items: ["a", null, "c"] },
    options: { arrayFormat: "comma" },
    expected: "items=a,c",  // was "items=a,,c"
},
{
    description: "should skip undefined values in comma format",
    input: { items: ["a", undefined, "c"] },
    options: { arrayFormat: "comma" },
    expected: "items=a,c",  // was "items=a,,c"
},
{
    description: "should produce empty string for all-null array in comma format",
    input: { items: [null, undefined] },
    options: { arrayFormat: "comma" },
    expected: "",
},

The qs library with { arrayFormat: "comma" } skips undefined values entirely, so ["a", undefined, "c"]"a,c". Our implementation now matches this.

description: "should handle null values in comma format",
input: { items: ["a", null, "c"] },
options: { arrayFormat: "comma" },
expected: "items=a,,c",
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

does this match qs behavior?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same as above — already fixed in edfea2cdc7e. The diff shown here is from an older commit; the latest code skips null/undefined entirely (matching qs library behavior).

Comment on lines +140 to +153
.add("limit", _queryParams.limit)
.add("id", _queryParams.id)
.add("date", _queryParams.date)
.add("deadline", _queryParams.deadline)
.add("bytes", _queryParams.bytes)
.add("user", _queryParams.user)
.add("userList", _queryParams.userList)
.add("optionalDeadline", _queryParams.optionalDeadline)
.add("keyValue", _queryParams.keyValue)
.add("optionalString", _queryParams.optionalString)
.add("nestedUser", _queryParams.nestedUser)
.add("optionalUser", _queryParams.optionalUser)
.add("excludeUser", _queryParams.excludeUser)
.add("filter", _queryParams.filter)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

All the non-comma style queryparams could be added in one method, like .addMany or something similar

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Implemented in b265758 — added addMany(params: Record<string, unknown>) to QueryStringBuilder. The generator now emits:

core.url.queryBuilder()
    .addMany(_queryParams)                                    // all params with default repeat format
    .add("tags", _queryParams.tags, { style: "comma" })       // comma overrides
    .add("optionalTags", _queryParams.optionalTags, { style: "comma" })
    .mergeAdditional(requestOptions?.queryParams)
    .build()

This replaces 15+ individual .add() calls with a single .addMany() + only the comma-style overrides. All 207 seed fixtures regenerated.

Swimburger and others added 9 commits April 15, 2026 23:09
…tion

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…s to all ts-sdk seed fixtures

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- convertParameters.ts: extract array examples via getParameterExample()
- ExampleEndpointFactory.ts: accept non-primitive query param examples
  (arrays, objects) instead of aborting the entire endpoint
- buildEndpointExample.ts: preserve full array examples instead of
  flattening to first element
- Wire test now uses tags: ["ACCESS_GRANTED", "COPY"] from spec

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…i examples

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ally

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant