Allow users to customize the name of all top level generated structures, like Client or ServerInterface - #2511
Conversation
Introduce `ComponentNames`, the configuration surface for renaming the fixed, spec-independent package-level identifiers oapi-codegen emits (`ServerInterface`, `Client`, `GetSwagger`, the parameter-binding error types, ...). This commit adds the struct, the three-layer resolution (defaults -> prefix -> explicit overrides, then family derivation from the resolved roots) and validation; the templates still hardcode their names and are converted in follow-ups, so generated output is unchanged. Resolution happens once in Generate and fills the struct in place, the same way `client-type-name` has always been defaulted. Unexported names (`swaggerSpec`, `rawSpec`, `decodeSpec`, `decodeSpecCached`, `strictHandler`) lower the prefix's first letter so they stay unexported, which is what makes emitting two specs into one Go package a one-line fix. `client-type-name` is generalized rather than duplicated: it keeps its narrow meaning (rename only the client struct) for backwards compatibility, while `component-names.client` is the root that renames the whole client family. Setting both to different values is a validation error. Validation covers Go-identifier legality of every supplied name, the prefix having to start with a letter, and pairwise uniqueness of the resolved names. Uniqueness is checked over the names a config actually emits, gated by `generate`, so disabling a generator cannot produce a spurious clash. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t-type-name Two amendments to the resolution model: The prefix is now prepended to every resolved name, an explicit override included, rather than only to names still at their default. Prefix is independent of the overrides: it affects everything or nothing. So `prefix: PetStore` with `client: MyClient` resolves to `PetStoreMyClient`, and the client family derives from that prefixed root. The unexported-name rule is unchanged: the prefix's first letter is lowered so `swaggerSpec` & friends stay unexported. `client-type-name` is deprecated rather than conflicting. When it and `component-names.client` are both set, `component-names.client` wins and Configuration.Warnings reports the shadowing; on its own it keeps seeding the client name exactly as before, renaming the struct and nothing else. ClientStem records the name the client family derives from, which differs from Client only under the deprecated knob. Templates use it for prose about the family as a whole, so that prose stays consistent with the family's names instead of drifting to the struct's. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…plates Replace the hardcoded client-family identifiers in client.tmpl and client-with-responses.tmpl with the resolved component names, doc comments included. The `client-type-name` interpolation that these templates already carried is unified with the new mechanism rather than layered on top of it: `$clientTypeName` now reads `names.Client`, which is where the legacy knob lands after resolution. Output is unchanged for every existing configuration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…plates Replace the hardcoded identifiers in the shared net/http skeletons (server-interface, server-middleware, server-handler), the chi and gorilla hooks, and the echo/gin/fiber/iris templates with the resolved component names: ServerInterface and its wrapper, MiddlewareFunc, the Handler and RegisterHandlers families, Unimplemented, ServeMux, EchoRouter, the per-framework *ServerOptions structs, and the six parameter-binding error types -- at their declarations, at every use site, and in the doc comments that name them. Note that echo declares no MiddlewareFunc of its own (it uses echo.MiddlewareFunc), so that name is dropped from echo's emitted set. The only change to generated output is a typo in one iris doc comment: `IrisServerOption` was missing its trailing `s`, and interpolating the type name necessarily corrects it. Every other fixture is byte-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… names
Completes the template conversion:
- strict server: StrictServerInterface, StrictHandlerFunc,
StrictMiddlewareFunc, NewStrictHandler(WithOptions), the unexported
strictHandler and the Strict{HTTP,Gin}ServerOptions structs, across all
five per-framework strict templates.
- embedded spec: GetSwagger, GetSpec, GetSpecJSON, PathToRawSpec and the
unexported swaggerSpec, rawSpec, decodeSpec and decodeSpecCached, whose
prefixed forms are what let two specs share a Go package.
- webhook/callback initiators and receivers, whose Webhook/Callback prefix
now composes with the component prefix rather than competing with it:
the component prefix goes in front of the existing one, after the New/With
verb, matching how New<Client> is derived.
The one place that keeps a hardcoded name is the import-mapping call
`<pkg>.PathToRawSpec` in inline.tmpl. That name belongs to the referenced
package, whose component-names config this config cannot see, so the
default is the only defensible choice; a comment records why.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Component names are declared by the templates, never by a TypeDefinition, so the duplicate-typename check in GenerateTypes could not see them: a spec with `components/schemas/Client` silently emitted two `Client` declarations and failed at `go build` with no clue where the second one came from. Fold the resolved component names into that check and report the collision with both remedies -- x-go-name on the schema, or component-names (or its prefix) on the component. Only the names the configuration actually declares are reserved, so a schema called `Client` in a models-only config is still fine. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two configurations generate from one spec into a single Go package, each with its own `component-names.prefix` -- the multi-spec-one-package case the prefix exists for, and only possible because the prefix renames the unexported swaggerSpec, rawSpec, decodeSpec, decodeSpecCached and strictHandler too. Between them the runs declare every fixed name the templates can emit: models, client, std-http server, strict server and embedded spec under `PetStore` (with root overrides for the client, server interface, strict server interface, handler and spec accessors, an error-type rename, and the stdhttp serve-mux group), and a chi server with a renamed Unimplemented under `Admin`. The committed .gen.go files are the stale-literal detector: a name a template still hardcodes either fails to compile as an undefined identifier or is redeclared across the two runs. The smoke test drives the renamed client against the renamed strict server, asserts the renamed error type is what the wrapper hands to ErrorHandlerFunc, and loads both embedded specs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the configuration-schema.json entry (editor-only, but kept at parity with the Go structs) and a README section covering the three reasons to rename -- schema collisions, two specs in one package, house style -- the uniform prefix rule including the unexported-name casing, the table of roots and what derives from each, the prefix-only remainder, and the two collision errors. Also cross-reference it from the single-package import-mapping section, which is where readers hit the two-specs-one-package problem, and mark `client-type-name` deprecated in the schema description and in the custom-client-type example, which is kept as-is to pin that option's behaviour. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two client knobs were competing: `component-names.client` won and the deprecated `client-type-name` was ignored whenever both were set. They now control different things. `component-names.client` is the family root -- folded in before derivation, so it names ClientInterface, NewClient, ClientWithResponses and the rest, and, absent the deprecated knob, the struct too. `client-type-name` is a struct-name override applied after derivation: it renames the client struct and nothing else, exactly as it always has. Setting both is now legitimate rather than half-ignored. The struct takes the legacy name while the family derives from the root, so `client-type-name: George` with `client: APIClient` yields `NewAPIClient() (*George, error)` -- mixed naming, but coherent, and the migration path for a codebase whose callers still spell the old struct name. Warnings describes the mix instead of reporting a shadowed knob. The override still lands before the uniqueness check, so a `client-type-name` colliding with a name derived from the root is caught as a configuration error rather than a compile error. Generated output is unchanged for every existing configuration: with `client-type-name` alone -- the only form in the wild -- the family stays at its defaults exactly as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ClientStem existed to serve a single doc comment: the client family root, which differs from the client struct's name only when the deprecated `client-type-name` overrides the struct. No derived field renders the bare root -- they all carry a suffix or a New prefix -- so the comment could not be written against the materialized fields. Trade the field for a slightly imprecise comment. The family now derives directly from cn.Client inside resolve(), which still runs before resolveComponentNames applies `client-type-name` on top, so the orthogonal semantics are untouched: the deprecated knob renames the emitted struct and nothing else, and the George/APIClient and collision cases behave exactly as before. Behavior delta, deliberate and narrowly scoped to one doc comment: with `client-type-name` set, the comment on the ClientWithResponses constructor now names the struct rather than the family, reading "wraps APIClient" where it used to read "wraps Client". That renames one line in each of the two fixtures that exercise the deprecated knob -- examples/custom-client-type and internal/test/schemas/deprecated -- and nothing else. Under the defaults and under `component-names.client` the rendered text is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The import-mapping call `<pkg>.PathToRawSpec` was left at the default name on the reasoning that it belongs to the referenced package, whose config we cannot see. That made renaming it a documented limitation, and made a mismatch fail silently in the sense that the call compiled only by luck -- it linked when the referenced package happened not to rename anything. Resolve it from the referencing config instead, the same field the declaration uses. With consistent settings it links; with mismatched settings it fails loudly at compile time (`undefined: externalRef0.ZzPathToRawSpec`) rather than quietly depending on one side having left the name alone. That turns the limitation into a requirement, documented alongside the component-names reference and styled after the existing strict-server import-mapping constraint: specs that reference each other through import-mapping must share their component-names settings, prefix included. The rationale is in the passage too -- import-mapping splits the boilerplate of what is conceptually one spec across packages, so the pieces should be generated consistently, with x-go-name / x-go-type left as the per-schema escape hatch if individual names must diverge. No change to generated output: under default settings the resolved name is the default, so every import-mapping fixture is byte-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Greptile SummaryThis PR adds configurable names and prefixes for fixed generated identifiers, updates templates across supported clients and server backends, and adds schema-to-component collision diagnostics.
Confidence Score: 4/5The PR should not merge until webhook and callback component declarations participate in collision detection, otherwise valid-looking generation can still produce uncompilable Go. The new collision mechanism omits package-level event-family declarations that are emitted for realistic webhook and callback specs, leaving schema-name collisions undiagnosed and producing duplicate Go declarations. Files Needing Attention: pkg/codegen/component_names.go, pkg/codegen/templates/initiator.tmpl, pkg/codegen/templates/receiver-stdlib.tmpl
|
| Filename | Overview |
|---|---|
| pkg/codegen/component_names.go | Adds component-name resolution and collision accounting, but omits spec-dependent webhook and callback component families from the reserved-name set. |
| pkg/codegen/codegen.go | Integrates resolved names into generation and checks schema names against the configured component declarations. |
| pkg/codegen/configuration.go | Adds component-name configuration validation and warnings while preserving the legacy client-type option. |
| configuration-schema.json | Mirrors the new component-name configuration hierarchy and deprecation guidance. |
| pkg/codegen/templates/initiator.tmpl | Prefixes webhook and callback initiator declarations, whose spec-dependent names are not represented in collision detection. |
| pkg/codegen/templates/receiver-stdlib.tmpl | Prefixes receiver declarations and error types, including event-family names omitted from the reserved-name map. |
| pkg/codegen/component_names_test.go | Thoroughly covers fixed component resolution and collisions but lacks schema-collision cases for emitted webhook and callback families. |
Prompt To Fix All With AI
### Issue 1
pkg/codegen/component_names.go:396-399
**Event component collisions remain undetected**
When models and webhook or callback client/server code are generated together, schemas such as `WebhookInitiator` or `WebhookReceiverInterface` are not checked against the corresponding template-generated declarations, causing duplicate package-level identifiers that downstream `go build` rejects.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "feat(templates): resolve PathToRawSpec a..." | Re-trigger Greptile
…ll site The field comment still described the pre-resolution behavior (cross-package callers using the default name because the referenced config is unknowable). Since the call site interpolates the referencing config's own resolved name, the accurate statement is the documented constraint: import-mapped configs must share component-names settings, and mismatches fail to compile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add
output-options.component-names: configurable names for all fixed generated identifiersoapi-codegen emits a set of package-level identifiers whose names are fixed and
spec-independent:
ServerInterface,Client,GetSwagger,RegisterHandlers,the parameter-binding error types, and ~40 more. Until now they could not be
renamed (except the client struct, via
client-type-name). That causes two realproblems with no escape hatch:
components/schemas/Client(orRequiredParamError, …) generates two declarations of the same name. Nothingdetected this; the output simply didn't compile.
package — every fixed name collides, including unexported ones
(
swaggerSpec,decodeSpec, …).This PR adds
output-options.component-names: aprefixthat decorates everyfixed name, plus individual overrides for the names users most plausibly need to
control. It also adds generation-time collision detection between schema names
and component names.
With no
component-namesconfigured, output is byte-identical to before, withtwo deliberate exceptions listed under Behavior deltas.
Usage
Emitting two specs into one package is the flagship use case and needs only the
prefix:
→
PetStoreServerInterface/AdminServerInterface,PetStoreGetSwagger/AdminGetSwagger,petStoreSwaggerSpec/adminSwaggerSpec, and so on — nocollisions, one package.
Configuration reference
20 keys, all optional. Root keys rename a family: derived names follow
automatically. Names without a key are still covered by
prefix.prefixclientClient<C>Interface,<C>Option,New<C>,<C>WithResponses,<C>WithResponsesInterface,New<C>WithResponsesserver-interfaceServerInterface<S>Wrappermiddleware-funcMiddlewareFuncecho.MiddlewareFunc)handlerHandler<H>FromMux,<H>WithOptions,<H>FromMuxWithBaseURLregister-handlersRegisterHandlers…WithBaseURL,…WithOptions,…OptionsunimplementedUnimplementedstrict-server-interfaceStrictServerInterfaceget-swagger/get-spec/get-spec-jsonGetSwagger/GetSpec/GetSpecJSONerrors.required-param-error(+ 5 siblings)RequiredParamError, …echo.routerEchoRouterstdhttp.serve-muxServeMuxfiber.handler-middleware-funcHandlerMiddlewareFuncPrefix-only names (no individual key):
RequestEditorFn,HttpRequestDoer,WithHTTPClient/WithBaseURL/WithRequestEditorFn,StrictHandlerFunc,StrictMiddlewareFunc,NewStrictHandler(WithOptions), the per-framework*ServerOptionsstructs,PathToRawSpec, the unexported spec machinery(
swaggerSpec,rawSpec,decodeSpec,decodeSpecCached,strictHandler),and the webhook/callback initiator/receiver families.
Resolution semantics
Resolved once per generation, in three layers:
(
prefix: PetStore+client: MyClient→PetStoreMyClient). The prefixis uniform and independent: it affects everything or nothing.
Family derivation happens after prefixing, from the resolved root — so each
derived name carries the prefix exactly once (
NewPetStoreMyClient).Unexported names take the prefix with its first letter lowered
(
petStoreSwaggerSpec), preserving unexportedness.Validation (config-load time): every supplied name must be a valid Go
identifier; the prefix must start with a letter; the resolved names of the
generators actually enabled must be pairwise distinct.
client-type-nameis deprecatedThe two knobs are orthogonal, which preserves the legacy knob's historical
behavior exactly:
component-names.clientis the family root: it renames the client structand everything derived from it.
client-type-namerenames only the client struct, as it always has.Set alone, each behaves as documented above. Set together, the struct takes the
legacy name while the family derives from the root —
client-type-name: Georgeclient: APIClientyieldsfunc NewAPIClient(...) (*George, error). Nothingbreaks, a warning describes the mixed naming, and removing the deprecated knob
is the fix.
Collision detection
Component names now participate in duplicate-name checking. A schema that
resolves to the same identifier as a component (e.g. a schema named
Client)fails generation with a message naming both remedies:
x-go-nameon theschema, or
component-names/prefixon the component. Previously thisproduced uncompilable output with no diagnostic.
Import-mapping
Specs that reference each other via
import-mappingmust be generated with thesame
component-namessettings (prefix included). Import-mapping splits whatis conceptually one spec's boilerplate across packages; the pieces are one API
and should be generated consistently. The cross-package
PathToRawSpeccallsite interpolates the referencing config's resolved name, so consistent
settings just work and mismatched settings fail to compile with an
undefinederror. Per-schema
x-go-name/x-go-typeremain the escape hatch if individualnames truly must diverge. (Documented in the README alongside the analogous
strict-server constraint.)
Behavior deltas (deliberate, visible in fixtures)
IrisServerOptionwas missingits trailing
s; interpolating the type name necessarily fixes the typo.client-type-namefixtures: one doc-comment line each — theNewClientWithResponsescomment now names the configured client type(e.g. "wraps
APIClient") instead of the literal wordClient.Every other pre-existing fixture is byte-identical.
Design notes
naming decisions. Doc comments and error-message strings interpolate too.
(
NewPetStoreClient— verb first, from derivation) and prefix-only names(
PetStoreNewStrictHandler— prefix first). Unifying this was consideredand rejected: all names are unique either way, and the special-casing isn't
worth the generator complexity.
forever; adding keys later is cheap, removing them impossible).
Testing
uniqueness and identifier validation, legacy-knob orthogonality (including
the both-set case asserted through full
Generate()output), andlegacy-name-vs-derived-name collisions.
internal/test/naming/componentnames/: two configs generatedfrom one spec into one package (
PetStore+Adminprefixes, rootoverrides, error rename, framework group), committed output, compile + smoke
test. This fixture doubles as the stale-literal detector: a missed hardcoded
name fails compilation.
unprefixed names + build), plus webhook and callback specs.
Clienterrors; renamedcomponent makes it generate).
🤖 Generated with Claude Code