Skip to content

Allow users to customize the name of all top level generated structures, like Client or ServerInterface - #2511

Open
mromaszewicz wants to merge 12 commits into
oapi-codegen:mainfrom
mromaszewicz:feat/naming-customization
Open

Allow users to customize the name of all top level generated structures, like Client or ServerInterface#2511
mromaszewicz wants to merge 12 commits into
oapi-codegen:mainfrom
mromaszewicz:feat/naming-customization

Conversation

@mromaszewicz

Copy link
Copy Markdown
Member

Add output-options.component-names: configurable names for all fixed generated identifiers

oapi-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 real
problems with no escape hatch:

  • Schema collisions: a spec with components/schemas/Client (or
    RequiredParamError, …) generates two declarations of the same name. Nothing
    detected this; the output simply didn't compile.
  • Multiple specs, one package: two generate runs can never share a Go
    package — every fixed name collides, including unexported ones
    (swaggerSpec, decodeSpec, …).

This PR adds output-options.component-names: a prefix that decorates every
fixed 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-names configured, output is byte-identical to before, with
two deliberate exceptions listed under Behavior deltas.

Usage

output-options:
  component-names:
    prefix: PetStore            # prepended to EVERY fixed name
    client: APIClient           # renames the whole client family
    server-interface: PetStoreAPI
    errors:
      required-param-error: MissingParamError
    stdhttp:
      serve-mux: Mux

Emitting two specs into one package is the flagship use case and needs only the
prefix:

# petstore.cfg.yaml                     # admin.cfg.yaml
output-options:                         output-options:
  component-names:                        component-names:
    prefix: PetStore                        prefix: Admin

PetStoreServerInterface / AdminServerInterface, PetStoreGetSwagger /
AdminGetSwagger, petStoreSwaggerSpec / adminSwaggerSpec, and so on — no
collisions, 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.

Key Default Renames
prefix prepended to every fixed name (see rules below)
client Client the client family: <C>Interface, <C>Option, New<C>, <C>WithResponses, <C>WithResponsesInterface, New<C>WithResponses
server-interface ServerInterface + <S>Wrapper
middleware-func MiddlewareFunc (not emitted by echo, which uses echo.MiddlewareFunc)
handler Handler net/http family: <H>FromMux, <H>WithOptions, <H>FromMuxWithBaseURL
register-handlers RegisterHandlers + …WithBaseURL, …WithOptions, …Options
unimplemented Unimplemented chi's stub struct
strict-server-interface StrictServerInterface
get-swagger / get-spec / get-spec-json GetSwagger / GetSpec / GetSpecJSON embedded-spec accessors
errors.required-param-error (+ 5 siblings) RequiredParamError, … the six parameter-binding error types
echo.router EchoRouter
stdhttp.serve-mux ServeMux
fiber.handler-middleware-func HandlerMiddlewareFunc

Prefix-only names (no individual key): RequestEditorFn, HttpRequestDoer,
WithHTTPClient/WithBaseURL/WithRequestEditorFn, StrictHandlerFunc,
StrictMiddlewareFunc, NewStrictHandler(WithOptions), the per-framework
*ServerOptions structs, 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:

  1. Default — today's name.
  2. Explicit override — replaces the default for that component.
  3. Prefix — prepended to every resolved name, overridden or not
    (prefix: PetStore + client: MyClientPetStoreMyClient). The prefix
    is 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-name is deprecated

The two knobs are orthogonal, which preserves the legacy knob's historical
behavior exactly:

  • component-names.client is the family root: it renames the client struct
    and everything derived from it.
  • client-type-name renames 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: George

  • client: APIClient yields func NewAPIClient(...) (*George, error). Nothing
    breaks, 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-name on the
schema, or component-names/prefix on the component. Previously this
produced uncompilable output with no diagnostic.

Import-mapping

Specs that reference each other via import-mapping must be generated with the
same component-names settings (prefix included). Import-mapping splits what
is conceptually one spec's boilerplate across packages; the pieces are one API
and should be generated consistently. The cross-package PathToRawSpec call
site interpolates the referencing config's resolved name, so consistent
settings just work and mismatched settings fail to compile with an undefined
error. Per-schema x-go-name/x-go-type remain the escape hatch if individual
names truly must diverge. (Documented in the README alongside the analogous
strict-server constraint.)

Behavior deltas (deliberate, visible in fixtures)

  • 9 iris fixtures: one doc-comment line each — IrisServerOption was missing
    its trailing s; interpolating the type name necessarily fixes the typo.
  • 2 client-type-name fixtures: one doc-comment line each — the
    NewClientWithResponses comment now names the configured client type
    (e.g. "wraps APIClient") instead of the literal word Client.

Every other pre-existing fixture is byte-identical.

Design notes

  • Names are resolved in Go and interpolated by templates; templates make no
    naming decisions. Doc comments and error-message strings interpolate too.
  • Verb placement under a prefix intentionally differs between derived names
    (NewPetStoreClient — verb first, from derivation) and prefix-only names
    (PetStoreNewStrictHandler — prefix first). Unifying this was considered
    and rejected: all names are unique either way, and the special-casing isn't
    worth the generator complexity.
  • Derived names deliberately have no individual keys yet (config surface is
    forever; adding keys later is cheap, removing them impossible).

Testing

  • Unit tests: resolution layers, prefix casing rules, family derivation,
    uniqueness and identifier validation, legacy-knob orthogonality (including
    the both-set case asserted through full Generate() output), and
    legacy-name-vs-derived-name collisions.
  • New fixture internal/test/naming/componentnames/: two configs generated
    from one spec into one package (PetStore + Admin prefixes, root
    overrides, error rename, framework group), committed output, compile + smoke
    test. This fixture doubles as the stale-literal detector: a missed hardcoded
    name fails compilation.
  • All nine framework generators verified under a prefix (generate + grep for
    unprefixed names + build), plus webhook and callback specs.
  • Collision detection covered both ways (schema Client errors; renamed
    component makes it generate).

🤖 Generated with Claude Code

mromaszewicz and others added 11 commits August 5, 2026 21:34
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>
@mromaszewicz
mromaszewicz requested a review from a team as a code owner August 6, 2026 14:55
@mromaszewicz mromaszewicz added enhancement New feature or request notable changes Used for release notes to highlight these more highly labels Aug 6, 2026
@greptile-apps

greptile-apps Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This 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.

  • Adds component-name resolution, identifier validation, family derivation, and generator-aware uniqueness checks.
  • Replaces fixed template identifiers with resolved names across client, embedded-spec, event, router, and strict-server output.
  • Adds configuration schema, documentation, unit tests, and a multi-prefix compilation fixture.

Confidence Score: 4/5

The 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

Important Files Changed

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

Comment thread pkg/codegen/component_names.go
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request notable changes Used for release notes to highlight these more highly

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant