📖 7 min read (~ 1400 words).

Resolving $ref name conflicts

A Swagger definition is keyed by a single short name (#/definitions/Account), but a Go program routinely has several types that would map to that name — the same leaf declared in different packages, or a swagger:model Account override applied twice. codescan keys every definition by a compiler-unique identity (<package-path>/<name>) while it builds, then a final reduce stage projects each identity back to the shortest name that is still unique. The result is deterministic regardless of discovery or map-iteration order: no silent overwrite, no lost definition.

The panes below are backed by the test-covered docs/examples/shaping/nameconflicts package tree.

When names collide

Two packages each declare an Account, with entirely different fields:

// Account is the billing view of a customer account.
//
// swagger:model Account
type Account struct {
	// the current balance, in minor units
	Balance int64 `json:"balance"`
	// the ISO-4217 currency code
	Currency string `json:"currency"`
}

Full source: docs/examples/shaping/nameconflicts/billing/account.go

// Account is the identity view of a customer account.
//
// swagger:model Account
type Account struct {
	// the login email
	Email string `json:"email"`
	// whether the email has been verified
	Verified bool `json:"verified"`
}

Full source: docs/examples/shaping/nameconflicts/identity/account.go

A Dashboard model references both, so they are discovered together:

// Dashboard references the same-named Account from two packages plus both
// ledger entries, forcing all of them to be discovered together. The two
// Accounts collide on the short name and are deconflicted by package segment;
// the refs below point at the resolved names, never a bare "Account".
//
// swagger:model Dashboard
type Dashboard struct {
	Billing  billing.Account  `json:"billing"`
	Identity identity.Account `json:"identity"`
	// the ledger entry that kept the name "Entry"
	Primary ledger.Entry `json:"primary"`
	// the duplicate that reverted to its Go name "Reversal"
	Secondary ledger.Reversal `json:"secondary"`
}

Full source: docs/examples/shaping/nameconflicts/doc.go

Before name-identity, the two would have merged onto a single #/definitions/Account — a union of fields, last package wins, non-deterministically. Now each keeps its own definition and the references resolve to the deconflicted names:

{
  "description": "Dashboard references the same-named Account from two packages plus both\nledger entries, forcing all of them to be discovered together. The two\nAccounts collide on the short name and are deconflicted by package segment;\nthe refs below point at the resolved names, never a bare \"Account\".",
  "type": "object",
  "properties": {
    "billing": {
      "$ref": "#/definitions/BillingAccount"
    },
    "identity": {
      "$ref": "#/definitions/IdentityAccount"
    },
    "primary": {
      "$ref": "#/definitions/Entry"
    },
    "secondary": {
      "$ref": "#/definitions/Reversal"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/nameconflicts"
}

Full source: docs/examples/shaping/nameconflicts/testdata/dashboard.json

How codescan resolves them automatically

The reduce stage gives every reachable identity the shortest acceptable name:

  • A globally unique leaf is lifted to its bare name — byte-identical to the pre-feature output, so the common case sees zero churn.
  • A colliding leaf is qualified with the minimal-depth PascalCase concat of its nearest package segments (billing.Account / identity.AccountBillingAccount / IdentityAccount), deepening one segment at a time until the whole group is unique. A validate.colliding-model-name diagnostic records each rename.

Every emitted definition also carries an x-go-package extension recording the source package, so even identically-shaped collisions stay traceable:

{
  "type": "object",
  "title": "Account is the billing view of a customer account.",
  "properties": {
    "balance": {
      "description": "the current balance, in minor units",
      "type": "integer",
      "format": "int64",
      "x-go-name": "Balance"
    },
    "currency": {
      "description": "the ISO-4217 currency code",
      "type": "string",
      "x-go-name": "Currency"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/nameconflicts/billing"
}

Full source: docs/examples/shaping/nameconflicts/testdata/billingaccount.json

Info

The whole pass is a pure function of the reachable identity set, so the names are stable across runs — but they are derived from your package paths. Renaming or moving a package changes the segment used in a qualified name. Pin the names that matter (see below).

Same-package duplicates

A single package cannot own a definition name twice. If two Go types in the same package both claim swagger:model Entry, codescan keeps one (deterministically) and reverts the other to its Go type name, with a validate.duplicate-model-name diagnostic:

// Entry keeps the contested name: the definition is "Entry".
//
// swagger:model Entry
type Entry struct {
	Debit int64 `json:"debit"`
}

// Reversal also asks for "Entry". The name is already taken in this package, so
// it reverts to its Go name, "Reversal", and a diagnostic is raised.
//
// swagger:model Entry
type Reversal struct {
	Credit int64 `json:"credit"`
}

Full source: docs/examples/shaping/nameconflicts/ledger/ledger.go

Here Entry keeps the contested name and Reversal falls back to its Go name — the Dashboard refs above point at #/definitions/Entry and #/definitions/Reversal, never a merged Entry. This is a genuine authoring error (one package, one name); the fallback keeps the spec valid rather than silently dropping a model.

Referencing a model by leaf across packages

The type-name keywords — swagger:type, swagger:additionalProperties, and swagger:patternProperties — accept a bare leaf as their argument. codescan resolves it the same way the reduce stage does: the annotating type’s own package first, then uniquely across the scanned model set. A leaf unique in another package resolves to a $ref:

swagger:additionalProperties Widget
// Bag is an open object whose additional properties are catalog.Widget,
// named by the bare leaf "Widget" — resolved cross-package because that leaf is
// unique across the scanned model set.
//
// swagger:model Bag
// swagger:additionalProperties Widget
type Bag struct {
	ID string `json:"id"`
}

Full source: docs/examples/shaping/nameconflicts/doc.go

#/definitions/Bag
{
  "description": "named by the bare leaf \"Widget\" — resolved cross-package because that leaf is\nunique across the scanned model set.",
  "type": "object",
  "title": "Bag is an open object whose additional properties are catalog.Widget,",
  "properties": {
    "id": {
      "type": "string",
      "x-go-name": "ID"
    }
  },
  "additionalProperties": {
    "$ref": "#/definitions/Widget"
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/nameconflicts"
}

Full source: docs/examples/shaping/nameconflicts/testdata/bag.json

If the leaf matches a model in several packages it is ambiguous: the reference is dropped (never guessed) and a validate.ambiguous-type-name diagnostic is raised. Disambiguate with a same-package type or pin the target with a swagger:model <Name> override. The same leaf rule applies to the additionalProperties: / swagger:patternProperties value forms covered in Maps & free-form objects.

Keeping the exposed names under your control

The generated $ref names are part of your published contract, so the author — not the resolver — should decide the ones that matter:

  • Pin a public name with an explicit swagger:model <Name>. A pinned name is the identity’s leaf, so two pinned names that still collide are deconflicted by package segment exactly like inferred ones — pin distinct names for the types in your public surface.
  • Let auto-resolution handle the rest. Incidental or internal collisions get a valid, stable, package-qualified name with no action from you.

Tuning the qualified names

Two scanner options steer the rare, deep collisions:

OptionDefaultEffect
NameConcatBudget0.65Readability cutoff in [0,1] (lower is more readable). A collision group whose best flat concat scores above the budget becomes a candidate for the hierarchical fallback. Raise toward 1.0 to accept longer concats; lower to fall back sooner.
EmitHierarchicalNamesfalseOpt into the fallback: over-budget groups are emitted as nested container definitions (#/definitions/<pkg>/<Name>, each tagged with x-go-package) instead of a long flat concat.
Warning

EmitHierarchicalNames is off by default on purpose. A nested definition is a deep JSON pointer that only ExpandSpec resolves, and a definitions-enumerating consumer (e.g. go-swagger codegen, one model per entry) sees the container nodes rather than the models. The always-correct flat concat stays the default; enable the nested shape only when you prefer it for the over-budget tail.

When to tune vs. let it auto-resolve

  • Pin the names that appear in your public API contract — clients, generated SDKs, and hand-written $refs depend on them.
  • Let auto-resolution handle incidental collisions between internal types; the qualified names are valid and stable.
  • Reach for EmitHierarchicalNames only when a few collision groups have package names long enough to make the flat concat unwieldy, and your consumers resolve $ref pointers (rather than enumerating definitions).

What’s next