Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions configuration-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,11 @@
"type": "boolean",
"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.",
"default": false
},
"resolve-type-name-collisions": {
"type": "boolean",
"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.",
"default": false
}
}
},
Expand Down
7 changes: 7 additions & 0 deletions internal/test/issues/issue-200/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# yaml-language-server: $schema=../../../../configuration-schema.json
package: issue200
generate:
models: true
output: issue200.gen.go
output-options:
resolve-type-name-collisions: true
3 changes: 3 additions & 0 deletions internal/test/issues/issue-200/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package issue200

//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml
49 changes: 49 additions & 0 deletions internal/test/issues/issue-200/issue200.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions internal/test/issues/issue-200/issue200_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package issue200

import (
"testing"

"github.com/stretchr/testify/assert"
)

// TestDuplicateTypeNamesCompile verifies that when the same name "Bar" is used
// across components/schemas, components/parameters, components/responses,
// components/requestBodies, and components/headers, the codegen produces
// distinct, compilable types with component-based suffixes.
//
// If the auto-rename logic breaks, this test will fail to compile.
func TestDuplicateTypeNamesCompile(t *testing.T) {
// Schema type: Bar (no suffix, first definition wins)
_ = Bar{Value: ptr("hello")}

// Schema types with unique names (no collision)
_ = Bar2{Value: ptr(float32(1.0))}
_ = BarParam([]int{1, 2, 3})
_ = BarParam2([]int{4, 5, 6})

// Parameter type: BarParameter (was "Bar" in components/parameters)
_ = BarParameter("query-value")

// Response type: BarResponse (was "Bar" in components/responses)
_ = BarResponse{
Value1: &Bar{Value: ptr("v1")},
Value2: &Bar2{Value: ptr(float32(2.0))},
Value3: &BarParam{1},
Value4: &BarParam2{2},
}

// RequestBody type: BarRequestBody (was "Bar" in components/requestBodies)
_ = BarRequestBody{Value: ptr(42)}

// Operation-derived types
_ = PostFooParams{Bar: &Bar{}}
_ = PostFooJSONBody{Value: ptr(99)}
_ = PostFooJSONRequestBody{Value: ptr(100)}

assert.True(t, true, "all duplicate-named types resolved and compiled")
}

func ptr[T any](v T) *T {
return &v
}
80 changes: 80 additions & 0 deletions internal/test/issues/issue-200/spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
openapi: 3.0.1

info:
title: "Duplicate type names test"
version: 0.0.0

paths:
/foo:
post:
operationId: postFoo
parameters:
- $ref: '#/components/parameters/Bar'
requestBody:
$ref: '#/components/requestBodies/Bar'
responses:
200:
$ref: '#/components/responses/Bar'

components:
schemas:
Bar:
type: object
properties:
value:
type: string
Bar2:
type: object
properties:
value:
type: number
BarParam:
type: array
items:
type: integer
BarParam2:
type: array
items:
type: integer

headers:
Bar:
schema:
type: boolean

parameters:
Bar:
name: Bar
in: query
schema:
type: string

requestBodies:
Bar:
content:
application/json:
schema:
type: object
properties:
value:
type: integer

responses:
Bar:
description: Bar response
headers:
X-Bar:
$ref: '#/components/headers/Bar'
content:
application/json:
schema:
type: object
properties:
value1:
$ref: '#/components/schemas/Bar'
value2:
$ref: '#/components/schemas/Bar2'
value3:
$ref: '#/components/schemas/BarParam'
value4:
$ref: '#/components/schemas/BarParam2'
15 changes: 11 additions & 4 deletions pkg/codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,8 @@ func GenerateTypesForResponses(t *template.Template, responses openapi3.Response
return nil, fmt.Errorf("error making name for components/responses/%s: %w", responseName, err)
}

goType.DefinedComp = ComponentTypeResponse

typeDef := TypeDefinition{
JsonName: responseName,
Schema: goType,
Expand Down Expand Up @@ -724,6 +726,8 @@ func GenerateTypesForRequestBodies(t *template.Template, bodies map[string]*open
return nil, fmt.Errorf("error making name for components/schemas/%s: %w", requestBodyName, err)
}

goType.DefinedComp = ComponentTypeRequestBody

typeDef := TypeDefinition{
JsonName: requestBodyName,
Schema: goType,
Expand All @@ -750,15 +754,18 @@ func GenerateTypes(t *template.Template, types []TypeDefinition) (string, error)
m := map[string]TypeDefinition{}
var ts []TypeDefinition

if globalState.options.OutputOptions.ResolveTypeNameCollisions {
types = FixDuplicateTypeNames(types)
}

for _, typ := range types {
if prevType, found := m[typ.TypeName]; found {
// If type names collide, we need to see if they refer to the same
// exact type definition, in which case, we can de-dupe. If they don't
// match, we error out.
// If type names collide after auto-rename, we need to see if they
// refer to the same exact type definition, in which case, we can
// de-dupe. If they don't match, we error out.
if TypeDefinitionsEquivalent(prevType, typ) {
continue
}
// We want to create an error when we try to define the same type twice.
return "", fmt.Errorf("duplicate typename '%s' detected, can't auto-rename, "+
"please use x-go-name to specify your own name for one of them", typ.TypeName)
}
Expand Down
8 changes: 8 additions & 0 deletions pkg/codegen/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,14 @@ type OutputOptions struct {

// 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
PreferSkipOptionalPointerOnContainerTypes bool `yaml:"prefer-skip-optional-pointer-on-container-types,omitempty"`

// ResolveTypeNameCollisions, 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.
ResolveTypeNameCollisions bool `yaml:"resolve-type-name-collisions,omitempty"`
}

func (oo OutputOptions) Validate() map[string]string {
Expand Down
25 changes: 23 additions & 2 deletions pkg/codegen/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,22 @@ type Schema struct {

// The original OpenAPIv3 Schema.
OAPISchema *openapi3.Schema

DefinedComp ComponentType // Indicates which component section defined this type
}

// ComponentType is used to keep track of where a given schema came from, in order
// to perform type name collision resolution.
type ComponentType int

const (
ComponentTypeSchema = iota
ComponentTypeParameter
ComponentTypeRequestBody
ComponentTypeResponse
ComponentTypeHeader
)

func (s Schema) IsRef() bool {
return s.RefType != ""
}
Expand Down Expand Up @@ -311,6 +325,7 @@ func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) {
Description: schema.Description,
OAPISchema: schema,
SkipOptionalPointer: skipOptionalPointer,
DefinedComp: ComponentTypeSchema,
}

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

// We can process the schema through the generic schema processor
if param.Schema != nil {
return GenerateGoSchema(param.Schema, path)
schema, err := GenerateGoSchema(param.Schema, path)
schema.DefinedComp = ComponentTypeParameter
return schema, err
}

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

Expand All @@ -869,11 +887,14 @@ func paramToGoType(param *openapi3.Parameter, path []string) (Schema, error) {
return Schema{
GoType: "string",
Description: StringToGoComment(param.Description),
DefinedComp: ComponentTypeParameter,
}, nil
}

// For json, we go through the standard schema mechanism
return GenerateGoSchema(mt.Schema, path)
schema, err := GenerateGoSchema(mt.Schema, path)
schema.DefinedComp = ComponentTypeParameter
return schema, err
}

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