Skip to content
Open
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
53 changes: 53 additions & 0 deletions pkg/codegen/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,19 @@ type Property struct {
NeedsFormTag bool
Extensions map[string]any
Deprecated bool

// resolvedGoFieldName, when non-empty, overrides the computed Go field
// name. It is set by ResolvePropertyGoFieldNameCollisions so that when two
// properties would normalize to the same Go identifier, both the struct
// declaration and the generated marshalling boilerplate agree on the same
// (disambiguated) name. See issue #2495.
resolvedGoFieldName string
}

func (p Property) GoFieldName() string {
if p.resolvedGoFieldName != "" {
return p.resolvedGoFieldName
}
goFieldName := p.JsonFieldName
if extension, ok := p.Extensions[extGoName]; ok {
if extGoFieldName, err := extParseGoFieldName(extension); err == nil {
Expand Down Expand Up @@ -1317,6 +1327,10 @@ func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) {
// primitive's Go type and clears the struct-shaped fields;
// rebuilding `struct {}` here would clobber that.
if len(outSchema.Properties) > 0 || outSchema.HasAdditionalProperties || len(outSchema.UnionElements) > 0 {
// Disambiguate any Go field-name collisions before rendering, so
// the struct declaration and the generated marshalling
// boilerplate agree on the same names. See issue #2495.
ResolvePropertyGoFieldNameCollisions(&outSchema)
outSchema.GoType = GenStructFromSchema(outSchema)
}
}
Expand Down Expand Up @@ -1755,6 +1769,45 @@ func GenFieldsFromProperties(props []Property) []string {
return fields
}

// ResolvePropertyGoFieldNameCollisions disambiguates the Go field names of a
// schema's properties in place. Distinct JSON property names can normalize to
// the same Go identifier (e.g. "host-fqdn" and "host_fqdn" both become
// "HostFqdn"), which would emit two identically named struct fields and fail to
// compile with "redeclared in this block".
//
// The first property to claim a name keeps it; each later property that would
// collide gets an incrementing numeric suffix ("HostFqdn2", "HostFqdn3", ...),
// re-checked so a suffixed name cannot itself collide. The resolved name is
// stored on the Property (via resolvedGoFieldName) so that both the struct
// declaration and the generated marshal/unmarshal boilerplate — which all read
// GoFieldName() — stay consistent. Only the Go identifier changes; the JSON tag
// still uses the original JsonFieldName, so the wire format is unchanged.
//
// When the schema also renders a synthetic "AdditionalProperties" field, that
// name is reserved first so a real property named "additionalProperties" (or
// one using x-go-name: AdditionalProperties) is suffixed instead of colliding
// with it. See issue #2495.
func ResolvePropertyGoFieldNameCollisions(schema *Schema) {
seen := make(map[string]struct{}, len(schema.Properties)+1)
if schema.HasAdditionalProperties {
seen["AdditionalProperties"] = struct{}{}
}
for i := range schema.Properties {
name := schema.Properties[i].GoFieldName()
if _, exists := seen[name]; exists {
for n := 2; ; n++ {
candidate := fmt.Sprintf("%s%d", name, n)
if _, taken := seen[candidate]; !taken {
name = candidate
break
}
}
schema.Properties[i].resolvedGoFieldName = name
}
seen[name] = struct{}{}
}
}

func additionalPropertiesType(schema Schema) string {
addPropsType := schema.AdditionalPropertiesType.GoType
if schema.AdditionalPropertiesType.RefType != "" {
Expand Down
51 changes: 51 additions & 0 deletions pkg/codegen/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1083,3 +1083,54 @@ func TestParameterDefinitionSchemaType(t *testing.T) {
assert.Equal(t, []string{"string", "integer"}, paramWithTypes(&openapi3.Types{"string", "integer", "null"}).SchemaTypes())
assert.Nil(t, paramWithTypes(&openapi3.Types{"string"}).SchemaTypes())
}

// TestResolvePropertyGoFieldNameCollisions covers issue #2495:
// two JSON properties that differ only by separator ("host-fqdn" vs
// "host_fqdn") both normalize to the Go field name "HostFqdn", which previously
// emitted two identically named fields and failed to compile with "redeclared
// in this block". The first keeps the name; the second is suffixed. JSON tags
// keep the original property names. Resolution happens on the schema so struct
// and marshalling boilerplate share the same names.
func TestResolvePropertyGoFieldNameCollisions(t *testing.T) {
schema := Schema{
Properties: []Property{
{JsonFieldName: "host-fqdn", Schema: Schema{GoType: "string"}, Required: true},
{JsonFieldName: "host_fqdn", Schema: Schema{GoType: "string"}, Required: true},
},
}

ResolvePropertyGoFieldNameCollisions(&schema)

// distinct, stable Go identifiers via GoFieldName (used by both the struct
// and the templates)
assert.Equal(t, "HostFqdn", schema.Properties[0].GoFieldName())
assert.Equal(t, "HostFqdn2", schema.Properties[1].GoFieldName())

// the rendered fields carry the disambiguated names and original JSON tags
fields := GenFieldsFromProperties(schema.Properties)
require.Len(t, fields, 2)
assert.Contains(t, fields[0], "HostFqdn ")
assert.Contains(t, fields[0], `json:"host-fqdn"`)
assert.Contains(t, fields[1], "HostFqdn2 ")
assert.Contains(t, fields[1], `json:"host_fqdn"`)
}

// TestResolvePropertyGoFieldNameCollisions_ReservesAdditionalProperties ensures
// a real property that normalizes to "AdditionalProperties" does not collide
// with the synthetic AdditionalProperties field generated for objects that
// allow additional properties.
func TestResolvePropertyGoFieldNameCollisions_ReservesAdditionalProperties(t *testing.T) {
schema := Schema{
HasAdditionalProperties: true,
Properties: []Property{
{JsonFieldName: "additionalProperties", Schema: Schema{GoType: "string"}, Required: true},
},
}

ResolvePropertyGoFieldNameCollisions(&schema)

// the synthetic field owns "AdditionalProperties"; the real property is
// suffixed so the generated struct has no duplicate declaration.
assert.Equal(t, "AdditionalProperties2", schema.Properties[0].GoFieldName())
assert.Contains(t, GenFieldsFromProperties(schema.Properties)[0], `json:"additionalProperties"`)
}