Skip to content

Serialise list-typed query parameters as repeated key=value pairs - #541

Merged
Smaug123 merged 8 commits into
mainfrom
fix-list-query-stringification
Jul 10, 2026
Merged

Serialise list-typed query parameters as repeated key=value pairs#541
Smaug123 merged 8 commits into
mainfrom
fix-list-query-stringification

Conversation

@Smaug123

@Smaug123 Smaug123 commented Jul 9, 2026

Copy link
Copy Markdown
Owner

List- and array-typed [<Query>] parameters were stringified with .ToString(), producing F# list syntax in the URL — e.g. Gitea's NotifyGetList sent status-types=%5Bx%3B%20y%5D. An int[] query parameter didn't even compile under net9.0 nullness checking (ToString() on the array is string | null).

Each element now contributes its own key=value pair, individually escaped — the "multi" collection format, which is what every array query parameter in the Gitea spec declares, and RestEase's convention for enumerable query parameters. (csv/ssv/tsv/pipes collection formats remain unsupported; nothing consumes them here today.)

Mechanically, the query string becomes a runtime computation: each parameter contributes a string list component ([ "limit=10" ] for scalars, List.map/Seq.map for lists and arrays), and the concatenation is appended to the URL — with separator — only when nonempty. So an empty list contributes nothing, and a query consisting solely of empty lists leaves the URL bare. Required scalar parameters produce byte-identical URLs to before.

Tests were written first and observed failing: TestHttpClient/TestListQueryParam.fs covers repeated pairs, empty-list omission, bare-URL behaviour, per-element escaping, and the array case, driven through a generated client against a mock handler.

Note: this deliberately branches off main rather than stacking on #540, but both rewrite the same query-string codegen block in HttpClientGenerator.fs, so whichever merges second will conflict there. The reconciliation is to unify the component type: #540 builds string option components (Option.map/List.choose) for optional scalars; this PR builds string list components. The union is string list components throughout, with optional scalars mapped via Option.toList.

🤖 Generated with Claude Code

List- and array-typed [<Query>] parameters were stringified with
.ToString(), producing F# list syntax in the URL (e.g.
"status-types=%5Bx%3B%20y%5D" for Gitea's NotifyGetList); int[] didn't
even compile under net9.0 nullness checking. Each element now
contributes its own key=value pair (the "multi" collection format,
which is what every array parameter in the Gitea spec declares, and
RestEase's convention), individually escaped.

The query string is now computed at runtime as a list of per-parameter
string-list components, concatenated and appended to the URL (with
separator) only when nonempty, so an empty list contributes nothing
and an all-empty query leaves the URL bare. Required scalar parameters
produce byte-identical URLs to before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Smaug123 and others added 7 commits July 10, 2026 10:39
The `queryString` local we emit to hold the serialised query string is in
scope for the URI, body and header expressions, all of which may refer to
the method's own parameters. A parameter named `queryString` was therefore
captured: a `[<Path>] queryString` alongside any other query parameter
silently used the serialised query string as the path value, and the
generated code still compiled, so nothing caught it.

Allocate the binding's name from the set of parameter names instead, so it
cannot collide. The name is unchanged when no parameter shares it, so no
existing generated output moves.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the queryString name-allocation loop into a pure `freshName`
helper and pin its invariants with FsCheck: the allocated name is never
one already taken, a free name is returned unchanged (so an endpoint that
doesn't collide never moves its generated output), and we only ever
suffix the requested name. Extraction leaves all generated output byte
-identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…WoofWare.Myriad into fix-list-query-stringification
@Smaug123
Smaug123 enabled auto-merge (squash) July 10, 2026 12:38
@Smaug123
Smaug123 merged commit b075432 into main Jul 10, 2026
20 checks passed
@Smaug123
Smaug123 deleted the fix-list-query-stringification branch July 10, 2026 12:41
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.

1 participant