Skip to content

allOf: anonymous type name conflict when $ref'd schema has single-element allOf #1651

@michaelbeutler

Description

@michaelbeutler

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:

  1. A component schema (e.g., Baz) that uses nullable: true + type: object + allOf: [single element]
  2. That schema is referenced via $ref from a property on another schema (e.g., Bar.baz)
  3. The parent schema (Bar) is used in two different operations — once directly, and once inside another allOf merge

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions