What version of ogen are you using?
$ go list -m github.com/ogen-go/ogen
github.com/ogen-go/ogen v1.20.1
Can this issue be reproduced with the latest version?
Yes (tested on v1.20.1-25-g33930884, current main).
What did you do?
When a component schema uses allOf with a single element internally and is referenced via $ref from another schema that is itself used in an allOf merge, code generation fails with anonymous type name conflict.
Minimal reproduction spec:
openapi: 3.1.0
info:
title: allOf collision repro
version: v0.1.0
paths:
/foo:
get:
operationId: getFoo
responses:
"200":
description: ok
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Bar"
default:
description: Error
content:
application/json:
schema:
type: object
properties:
error:
type: string
/foo/extended:
get:
operationId: getFooExtended
responses:
"200":
description: ok
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
- type: object
properties:
data:
type: array
items:
allOf:
- $ref: "#/components/schemas/Bar"
- type: object
required:
- extra
properties:
extra:
type: string
default:
description: Error
content:
application/json:
schema:
type: object
properties:
error:
type: string
components:
schemas:
Baz:
nullable: true
type: object
allOf:
- type: object
required:
- id
- value
properties:
id:
type: string
format: uuid
value:
type: number
Bar:
type: object
required:
- name
- baz
properties:
name:
type: string
baz:
nullable: true
$ref: "#/components/schemas/Baz"
Run:
ogen --target ./out --clean repro.yml
What did you expect to see?
Successful code generation with two response types:
GetFooOK containing Bar items
GetFooExtendedOK containing Bar items extended with an extra field
- Both types reusing the same
Baz struct
What did you see instead?
anonymous type name conflict: "Baz"
Root cause: In gen/schema_gen_sum.go, the allOf() function has a fast-path for single-element allOf (line 1211):
if len(schema.AllOf) == 1 {
s := schema.AllOf[0]
if s != nil {
return g.generate(name, s, false)
}
}
It returns the inner allOf member directly, which is an anonymous schema without a Ref field set. The outer schema's Ref (e.g., #/components/schemas/Baz) is never transferred to the inner schema. This causes regtype() to save the type as an anonymous side type instead of a ref-keyed type.
When a second operation encounters the same schema via $ref, lookupRef() finds nothing (it was never saved by ref), so it regenerates the type under the same name, triggering the anonymous type name conflict at tstorage.merge().
Note: The multi-element allOf path (line 1218-1224) correctly preserves the Ref via mergedSchema.Ref = schema.Ref, but the single-element fast-path skips this.
Conditions to trigger:
- A component schema (e.g.,
Baz) that uses nullable: true + type: object + allOf: [single element]
- That schema is referenced via
$ref from a property on another schema (e.g., Bar.baz)
- The parent schema (
Bar) is used in two different operations — once directly, and once inside another allOf merge
What version of ogen are you using?
Can this issue be reproduced with the latest version?
Yes (tested on
v1.20.1-25-g33930884, currentmain).What did you do?
When a component schema uses
allOfwith a single element internally and is referenced via$reffrom another schema that is itself used in anallOfmerge, code generation fails withanonymous type name conflict.Minimal reproduction spec:
Run:
What did you expect to see?
Successful code generation with two response types:
GetFooOKcontainingBaritemsGetFooExtendedOKcontainingBaritems extended with anextrafieldBazstructWhat did you see instead?
Root cause: In
gen/schema_gen_sum.go, theallOf()function has a fast-path for single-elementallOf(line 1211):It returns the inner allOf member directly, which is an anonymous schema without a
Reffield set. The outer schema'sRef(e.g.,#/components/schemas/Baz) is never transferred to the inner schema. This causesregtype()to save the type as an anonymous side type instead of a ref-keyed type.When a second operation encounters the same schema via
$ref,lookupRef()finds nothing (it was never saved by ref), so it regenerates the type under the same name, triggering theanonymous type name conflictattstorage.merge().Note: The multi-element allOf path (line 1218-1224) correctly preserves the Ref via
mergedSchema.Ref = schema.Ref, but the single-element fast-path skips this.Conditions to trigger:
Baz) that usesnullable: true+type: object+allOf: [single element]$reffrom a property on another schema (e.g.,Bar.baz)Bar) is used in two different operations — once directly, and once inside anotherallOfmerge