Skip to content

[BUG][go-server] Duplicate auto-generated operationIds produce uncompilable output (route/handler/service name mismatch) #24167

Description

@halfcrazy

Bug Report Checklist

  • Have you provided a full/minimal spec to reproduce the issue?
  • Have you validated the input using an OpenAPI validator (example)?
  • Have you tested with the latest master to confirm the issue still exists?
  • Have you searched for related issues/PRs?
  • What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

When two paths share the same auto-generated operationId (e.g. /foo and /foo/, both GET, neither declaring an explicit operationId), the go-server generator produces output that does not compile.

The route map and the *Router/*Servicer interfaces (which use {{operationId}}) get the deduplicated name FooGet_0, but the controller handler method and the service stub method (which use {{nickname}}) get a different deduplicated name FooGet_1. The generated file then references names that don't exist:

// api_default.go (routes)
"FooGet_0": Route{ "GET", "/foo/", c.FooGet_0 },   // c.FooGet_0 does not exist

// api_default.go (handlers)
func (c *DefaultAPIController) FooGet_1(...) { ... c.service.FooGet_1(...) } // c.service.FooGet_1 not declared

// api.go (interface) declares FooGet_0 (not FooGet_1)

This is a problem because the generated server cannot be built at all without hand-editing the generated files. go-gin-server has the same {{nickname}}/{{operationId}} split and is affected too.

openapi-generator version
OpenAPI declaration file content or url
openapi: 3.0.1
info:
  title: repro
  version: '1.0'
paths:
  /foo:
    get:
      responses:
        '200':
          description: ok
  /foo/:
    get:
      responses:
        '200':
          description: ok
Generation Details

CLI, go-server generator, default options (only sourceFolder/outputAsLibrary set so the service stub is also generated and the mismatch is visible there too):

openapi-generator generate \
  -i repro.yaml \
  -g go-server \
  -o out/ \
  --package-name server \
  --enable-post-process-file \
  -p sourceFolder=server,outputAsLibrary=true
Steps to reproduce
  1. Save the spec above as repro.yaml.
  2. Run the generation command from Generation Details.
  3. Build the generated server:
cd out/server
go mod init repro
go get github.com/gorilla/mux
go build ./...

Actual output (build fails):

./api_default.go:59:6: c.FooGet_0 undefined (type *DefaultAPIController has no field or method FooGet_0)
./api_default.go:78:27: c.service.FooGet_1 undefined (type DefaultAPIServicer has no field or method FooGet_1)
./api_default_service.go:42:18: c.service.FooGet_1 undefined (type DefaultAPIServicer has no field or method FooGet_1)

Inspection of the generated files shows the divergence:

  • Routes (api_default.go): FooGet, FooGet_0 (route FooGet_0c.FooGet_0)
  • Handlers (api_default.go): FooGet, FooGet_1 (handler FooGet_1 calls c.service.FooGet_1)
  • Interfaces (api.go): FooGet, FooGet_0 (both *Router and *Servicer)
  • Service stub (api_default_service.go): FooGet, FooGet_1

Expected output: nickname and operationId stay consistent after deduplication, so routes / interfaces / handlers / service stubs all use FooGet and FooGet_0. go build ./... succeeds.

Related issues/PRs
Suggest a fix

Root cause: there are two independent deduplication passes with inconsistent counter conventions, and the go-server templates use both fields in the same compilation unit.

  1. DefaultCodegen.addOperationToGroup deduplicates operationIdfirst duplicate gets _0:

    int counter = 0;
    for (CodegenOperation op : opList) {
        if (uniqueName.equals(op.operationId)) {
            uniqueName = co.operationId + "_" + counter; // _0
            counter++;
        }
    }
    co.operationId = uniqueName;
  2. DefaultGenerator.processOperations (and processWebhooks) separately deduplicates nickname (gated by addSuffixToDuplicateOperationNicknames, which defaults to true in DefaultCodegen) — first duplicate gets _1:

    int counter = 0;
    for (CodegenOperation op : ops) {
        String opId = op.nickname;
        if (opIds.contains(opId)) {
            counter++;                       // 1
            op.nickname += "_" + counter;    // _1
        }
        opIds.add(opId);
    }
  3. The go-server templates split on the two fields: controller-api.mustache uses {{operationId}} for the route map (c.{{operationId}}) but {{nickname}} for the handler method and the c.service.{{nickname}} call; api.mustache uses {{operationId}} for both interfaces; service.mustache uses {{nickname}} for the stub method.

Since operationId is already unique per tag after addOperationToGroup, the second nickname pass is redundant; it should mirror operationId instead of re-deduplicating with a different counter. In DefaultGenerator.processOperations / processWebhooks:

// check for nickname uniqueness
if (config.getAddSuffixToDuplicateOperationNicknames()) {
    // operationId is already deduplicated per tag in addOperationToGroup,
    // so mirror it into nickname. The previous separate _<counter> dedup used a
    // different counter convention (_1 vs _0) and diverged nickname from
    // operationId, which breaks templates (e.g. go-server) that use {{nickname}}
    // for handlers/stubs and {{operationId}} for routes/interfaces.
    for (CodegenOperation op : ops) {
        op.nickname = op.operationId;
    }
}

This fixes every generator whose templates use {{nickname}} (go-server, go-gin-server, …) in one place, and is a no-op for non-duplicate operationIds (where nickname == operationId already).

A narrower alternative is to switch the go-server / go-gin-server templates from {{nickname}} to {{operationId}} everywhere (controller-api.mustache + service.mustache for go-server; controller-api.mustache + interface-api.mustache for go-gin-server), but the DefaultGenerator fix above is more fundamental and matches the intent of addSuffixToDuplicateOperationNicknames.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions