Skip to content

feat: camelCase query keys by default, with a naming policy - #8

Merged
Katyara11 merged 1 commit into
mainfrom
feat/query-naming-policy
Nov 7, 2026
Merged

Katyara11 merged 1 commit into
mainfrom
feat/query-naming-policy

Conversation

@Katyara11

Copy link
Copy Markdown
Member

Summary

Query keys are camelCased from their identifiers by default, and a [QueryNaming] policy covers
APIs following another convention — snake_case for a Python or Ruby backend being the usual case.

Changes

  • QueryNaming enum, [QueryNaming] on an interface, [Query(Naming = …)] per parameter.
  • CamelCase (default), SnakeCaseLower/Upper, KebabCaseLower/Upper, AsDeclared.
  • Docs: defining-a-client.md, package README, CHANGELOG.

Type

  • Feature
  • Bug fix
  • Refactor
  • Chore (CI, deps, config)

Checklist

  • Code builds without errors
  • Tests added/updated

Notes for review

Why the default changed. Expansion shipped sending property names verbatim, reasoned from
[Query] parameters doing the same. That compared against the wrong neighbour: C# parameter names
are camelCase by convention, so "verbatim" only looks like camelCase there. The closer neighbour
is body serialization, which camelCases — so the same type produced {"state":…} as a [Body] and
?State=… as a [Query]. Most APIs want the camelCase one, so the old default cost an attribute
on nearly every property of every filter — the friction the feature existed to remove.

It applies to parameters too. A policy that transformed object fields but left parameter names
alone would leave half a query string in the other convention — worse than either consistent
choice, and the exact trap someone setting snake_case would fall into. In practice the default
changes nothing for parameters (C# names are already camelCase); a non-camelCase one now
camelCases, and AsDeclared is the one-attribute escape.

Word splitting follows what a reader expects, not "uppercase starts a word": HTTPReferer is
http_referer, not h_t_t_p_referer; Top10Items is top_10_items. The camelCase rule matches
JsonNamingPolicy.CamelCase exactly, since matching it is the justification for the default.

Compile-time. Applied in the generator, so every key is a literal in the emitted code —
AsDeclared is not a performance choice. Dictionary keys are untouched, being data rather than a
derived identifier.

Three existing tests asserted the old behaviour, one of them specifically to pin it. They now pin
the new default, with separate coverage for AsDeclared.

Verification

1500 tests; --warnaserror clean, dotnet format clean, native AOT publishes and runs.

The expansion landed sending property names verbatim, reasoned from [Query]
parameters already sending theirs that way. Coherent, but compared against the
wrong neighbour: C# parameter names are camelCase by convention, so "verbatim"
only looks like camelCase there. The closer neighbour is body serialization,
which camelCases — so the same filter type sent as [Body] produced
{"state":...} while as [Query] it produced ?State=..., one library spelling one
type two ways. Since most APIs want the camelCase one, the old default cost an
attribute on nearly every property of every filter object, which is the
friction the feature existed to remove.

snake_case then makes a hardcoded flip the wrong shape, so this is a policy:
[QueryNaming] on the interface for the API that follows one convention
throughout — a Python or Ruby backend being the usual reason — overridable per
parameter and always beaten by an explicit [Query("name")].

It applies to parameter names as well as object properties. A policy that
transformed fields but left parameters alone would leave half a query string in
the other convention, which is worse than either consistent choice and is
exactly the trap someone setting snake_case would fall into. The default
changes nothing for parameters in practice, since C# parameter names are
already camelCase; AsDeclared is the one-attribute escape for anyone relying on
a non-camelCase one.

Word splitting follows what a reader expects rather than "uppercase starts a
word": an acronym run is one word and a digit run is its own, so HTTPReferer is
http_referer and not h_t_t_p_referer, and Top10Items is top_10_items. The
camelCase rule matches JsonNamingPolicy.CamelCase exactly, since matching it is
the whole justification for the default.

Applied in the generator, so every key is a literal in the emitted code and
nothing is transformed per request — AsDeclared is not a performance choice.
Dictionary keys are left alone, being data rather than a derived identifier.

Three existing tests asserted the old behaviour, including one whose entire
purpose was to pin it; they now pin the new default, and AsDeclared has its own
coverage. 1500 tests; build, format and native AOT clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Katyara11
Katyara11 force-pushed the feat/query-naming-policy branch from 7bd31fd to d8dc2b0 Compare November 7, 2026 11:45
@Katyara11
Katyara11 merged commit fe9ba9a into main Nov 7, 2026
2 checks passed
@Katyara11
Katyara11 deleted the feat/query-naming-policy branch November 7, 2026 11:47
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