Skip to content
This repository was archived by the owner on Apr 14, 2026. It is now read-only.

Commit 13eb5ac

Browse files
mgurevinmromaszewiczclaude
authored
fixed duplicate type names (oapi-codegen#200)
* fixed duplicate type names * add typename dedup functions * Fixup: use full-word suffixes and add regression test for issue oapi-codegen#200 - Rename auto-dedup suffixes to use full words: Parameter, Response, RequestBody (instead of Param, Resp, ReqBody) - Add internal/test/issues/issue-200/ with spec, config, generated code, and a compile-time regression test that instantiates every expected type Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Gate duplicate type name resolution behind output-options config flag Add ResolveTypeNameCollisions bool to OutputOptions and the JSON schema. When false (the default), the codegen errors on duplicate type names as before. When true, FixDuplicateTypeNames auto-renames colliding types. Also cleans up ComponentType: removes unused constants, improves doc. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Marcin Romaszewicz <marcinr@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5f38641 commit 13eb5ac

10 files changed

Lines changed: 317 additions & 6 deletions

File tree

configuration-schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,11 @@
252252
"type": "boolean",
253253
"description": "Allows disabling the generation of an 'optional pointer' for an optional field that is a container type (such as a slice or a map), which ends up requiring an additional, unnecessary, `... != nil` check. A field can set `x-go-type-skip-optional-pointer: false` to still require the optional pointer.",
254254
"default": false
255+
},
256+
"resolve-type-name-collisions": {
257+
"type": "boolean",
258+
"description": "When set to true, automatically renames types that collide across different OpenAPI component sections (schemas, parameters, requestBodies, responses, headers) by appending a suffix based on the component section (e.g., 'Parameter', 'Response', 'RequestBody'). Without this, the codegen will error on duplicate type names, requiring manual resolution via x-go-name.",
259+
"default": false
255260
}
256261
}
257262
},
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# yaml-language-server: $schema=../../../../configuration-schema.json
2+
package: issue200
3+
generate:
4+
models: true
5+
output: issue200.gen.go
6+
output-options:
7+
resolve-type-name-collisions: true
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package issue200
2+
3+
//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml

internal/test/issues/issue-200/issue200.gen.go

Lines changed: 49 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package issue200
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
// TestDuplicateTypeNamesCompile verifies that when the same name "Bar" is used
10+
// across components/schemas, components/parameters, components/responses,
11+
// components/requestBodies, and components/headers, the codegen produces
12+
// distinct, compilable types with component-based suffixes.
13+
//
14+
// If the auto-rename logic breaks, this test will fail to compile.
15+
func TestDuplicateTypeNamesCompile(t *testing.T) {
16+
// Schema type: Bar (no suffix, first definition wins)
17+
_ = Bar{Value: ptr("hello")}
18+
19+
// Schema types with unique names (no collision)
20+
_ = Bar2{Value: ptr(float32(1.0))}
21+
_ = BarParam([]int{1, 2, 3})
22+
_ = BarParam2([]int{4, 5, 6})
23+
24+
// Parameter type: BarParameter (was "Bar" in components/parameters)
25+
_ = BarParameter("query-value")
26+
27+
// Response type: BarResponse (was "Bar" in components/responses)
28+
_ = BarResponse{
29+
Value1: &Bar{Value: ptr("v1")},
30+
Value2: &Bar2{Value: ptr(float32(2.0))},
31+
Value3: &BarParam{1},
32+
Value4: &BarParam2{2},
33+
}
34+
35+
// RequestBody type: BarRequestBody (was "Bar" in components/requestBodies)
36+
_ = BarRequestBody{Value: ptr(42)}
37+
38+
// Operation-derived types
39+
_ = PostFooParams{Bar: &Bar{}}
40+
_ = PostFooJSONBody{Value: ptr(99)}
41+
_ = PostFooJSONRequestBody{Value: ptr(100)}
42+
43+
assert.True(t, true, "all duplicate-named types resolved and compiled")
44+
}
45+
46+
func ptr[T any](v T) *T {
47+
return &v
48+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
openapi: 3.0.1
2+
3+
info:
4+
title: "Duplicate type names test"
5+
version: 0.0.0
6+
7+
paths:
8+
/foo:
9+
post:
10+
operationId: postFoo
11+
parameters:
12+
- $ref: '#/components/parameters/Bar'
13+
requestBody:
14+
$ref: '#/components/requestBodies/Bar'
15+
responses:
16+
200:
17+
$ref: '#/components/responses/Bar'
18+
19+
components:
20+
schemas:
21+
Bar:
22+
type: object
23+
properties:
24+
value:
25+
type: string
26+
Bar2:
27+
type: object
28+
properties:
29+
value:
30+
type: number
31+
BarParam:
32+
type: array
33+
items:
34+
type: integer
35+
BarParam2:
36+
type: array
37+
items:
38+
type: integer
39+
40+
headers:
41+
Bar:
42+
schema:
43+
type: boolean
44+
45+
parameters:
46+
Bar:
47+
name: Bar
48+
in: query
49+
schema:
50+
type: string
51+
52+
requestBodies:
53+
Bar:
54+
content:
55+
application/json:
56+
schema:
57+
type: object
58+
properties:
59+
value:
60+
type: integer
61+
62+
responses:
63+
Bar:
64+
description: Bar response
65+
headers:
66+
X-Bar:
67+
$ref: '#/components/headers/Bar'
68+
content:
69+
application/json:
70+
schema:
71+
type: object
72+
properties:
73+
value1:
74+
$ref: '#/components/schemas/Bar'
75+
value2:
76+
$ref: '#/components/schemas/Bar2'
77+
value3:
78+
$ref: '#/components/schemas/BarParam'
79+
value4:
80+
$ref: '#/components/schemas/BarParam2'

pkg/codegen/codegen.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,8 @@ func GenerateTypesForResponses(t *template.Template, responses openapi3.Response
673673
return nil, fmt.Errorf("error making name for components/responses/%s: %w", responseName, err)
674674
}
675675

676+
goType.DefinedComp = ComponentTypeResponse
677+
676678
typeDef := TypeDefinition{
677679
JsonName: responseName,
678680
Schema: goType,
@@ -724,6 +726,8 @@ func GenerateTypesForRequestBodies(t *template.Template, bodies map[string]*open
724726
return nil, fmt.Errorf("error making name for components/schemas/%s: %w", requestBodyName, err)
725727
}
726728

729+
goType.DefinedComp = ComponentTypeRequestBody
730+
727731
typeDef := TypeDefinition{
728732
JsonName: requestBodyName,
729733
Schema: goType,
@@ -750,15 +754,18 @@ func GenerateTypes(t *template.Template, types []TypeDefinition) (string, error)
750754
m := map[string]TypeDefinition{}
751755
var ts []TypeDefinition
752756

757+
if globalState.options.OutputOptions.ResolveTypeNameCollisions {
758+
types = FixDuplicateTypeNames(types)
759+
}
760+
753761
for _, typ := range types {
754762
if prevType, found := m[typ.TypeName]; found {
755-
// If type names collide, we need to see if they refer to the same
756-
// exact type definition, in which case, we can de-dupe. If they don't
757-
// match, we error out.
763+
// If type names collide after auto-rename, we need to see if they
764+
// refer to the same exact type definition, in which case, we can
765+
// de-dupe. If they don't match, we error out.
758766
if TypeDefinitionsEquivalent(prevType, typ) {
759767
continue
760768
}
761-
// We want to create an error when we try to define the same type twice.
762769
return "", fmt.Errorf("duplicate typename '%s' detected, can't auto-rename, "+
763770
"please use x-go-name to specify your own name for one of them", typ.TypeName)
764771
}

pkg/codegen/configuration.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,14 @@ type OutputOptions struct {
300300

301301
// PreferSkipOptionalPointerOnContainerTypes allows disabling the generation of an "optional pointer" for an optional field that is a container type (such as a slice or a map), which ends up requiring an additional, unnecessary, `... != nil` check
302302
PreferSkipOptionalPointerOnContainerTypes bool `yaml:"prefer-skip-optional-pointer-on-container-types,omitempty"`
303+
304+
// ResolveTypeNameCollisions, when set to true, automatically renames
305+
// types that collide across different OpenAPI component sections
306+
// (schemas, parameters, requestBodies, responses, headers) by appending
307+
// a suffix based on the component section (e.g., "Parameter", "Response",
308+
// "RequestBody"). Without this, the codegen will error on duplicate type
309+
// names, requiring manual resolution via x-go-name.
310+
ResolveTypeNameCollisions bool `yaml:"resolve-type-name-collisions,omitempty"`
303311
}
304312

305313
func (oo OutputOptions) Validate() map[string]string {

pkg/codegen/schema.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,22 @@ type Schema struct {
3939

4040
// The original OpenAPIv3 Schema.
4141
OAPISchema *openapi3.Schema
42+
43+
DefinedComp ComponentType // Indicates which component section defined this type
4244
}
4345

46+
// ComponentType is used to keep track of where a given schema came from, in order
47+
// to perform type name collision resolution.
48+
type ComponentType int
49+
50+
const (
51+
ComponentTypeSchema = iota
52+
ComponentTypeParameter
53+
ComponentTypeRequestBody
54+
ComponentTypeResponse
55+
ComponentTypeHeader
56+
)
57+
4458
func (s Schema) IsRef() bool {
4559
return s.RefType != ""
4660
}
@@ -311,6 +325,7 @@ func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) {
311325
Description: schema.Description,
312326
OAPISchema: schema,
313327
SkipOptionalPointer: skipOptionalPointer,
328+
DefinedComp: ComponentTypeSchema,
314329
}
315330

316331
// AllOf is interesting, and useful. It's the union of a number of other
@@ -849,7 +864,9 @@ func paramToGoType(param *openapi3.Parameter, path []string) (Schema, error) {
849864

850865
// We can process the schema through the generic schema processor
851866
if param.Schema != nil {
852-
return GenerateGoSchema(param.Schema, path)
867+
schema, err := GenerateGoSchema(param.Schema, path)
868+
schema.DefinedComp = ComponentTypeParameter
869+
return schema, err
853870
}
854871

855872
// At this point, we have a content type. We know how to deal with
@@ -859,6 +876,7 @@ func paramToGoType(param *openapi3.Parameter, path []string) (Schema, error) {
859876
return Schema{
860877
GoType: "string",
861878
Description: StringToGoComment(param.Description),
879+
DefinedComp: ComponentTypeParameter,
862880
}, nil
863881
}
864882

@@ -869,11 +887,14 @@ func paramToGoType(param *openapi3.Parameter, path []string) (Schema, error) {
869887
return Schema{
870888
GoType: "string",
871889
Description: StringToGoComment(param.Description),
890+
DefinedComp: ComponentTypeParameter,
872891
}, nil
873892
}
874893

875894
// For json, we go through the standard schema mechanism
876-
return GenerateGoSchema(mt.Schema, path)
895+
schema, err := GenerateGoSchema(mt.Schema, path)
896+
schema.DefinedComp = ComponentTypeParameter
897+
return schema, err
877898
}
878899

879900
func generateUnion(outSchema *Schema, elements openapi3.SchemaRefs, discriminator *openapi3.Discriminator, path []string) error {

0 commit comments

Comments
 (0)