Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ malformed input, the petstore, aliased schemas, go123-specific forms, and cross-
- `codescan.Run(*Options) (*spec.Swagger, error)` — the main entry point.
- `codescan.Options` — configuration. Notable fields beyond `Packages`/`WorkDir`:
- `ScanModels` — also emit definitions for `swagger:model` types.
- `PruneUnusedModels` — with `ScanModels`, prune discovered definitions not
transitively referenced from any path/response/parameter/overlay root.
Runs before name reduction (so an unused model can't force a spurious
collision rename on a used one); `InputSpec` definitions are pinned. Each
drop raises a `scan.pruned-unused` Hint; collision renames raise
`scan.renamed-definition`. See `internal/scanner/README.md#prune`.
- `InputSpec` — overlay: merge discoveries on top of an existing spec.
- `BuildTags`, `Include`/`Exclude`, `IncludeTags`/`ExcludeTags`, `ExcludeDeps` — scope control.
- `RefAliases`, `TransparentAliases`, `DescWithRef` — alias handling knobs
Expand Down
105 changes: 105 additions & 0 deletions docs/doc-site/shaping-the-output/pruning-unused-models.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
title: Pruning unused models
weight: 16
description: |
Scan a shared library with swagger:model discovery, then keep only the
definitions actually reachable from your API — the middle ground between
"only what routes use" and "every model, used or not".
---

`Options.ScanModels` (the `-m` flag) publishes **every** `swagger:model` type it
finds, whether or not anything references it — see
[When the scanner emits a type]({{% relref "/shaping-the-output/type-discovery" %}}).
That is exactly what you want when the annotated package *is* the contract. It is
the wrong default when you point codescan at a large **shared model library** and
only care about the slice your API actually exposes: the spec fills up with
definitions no operation, parameter or response ever references.

`Options.PruneUnusedModels` is the middle ground. It runs `swagger:model`
discovery as usual, then drops every discovered definition that is not
reachable from your API surface.

## Three emission modes

The same source renders three ways, depending on two options:

| Mode | Options | What is emitted |
|---|---|---|
| **Reachable only** | *(default)* | Only models reachable from an operation, parameter or response — discovery-driven. A `swagger:model` that nothing references is **not** emitted. |
| **Every model** | `ScanModels` | Every `swagger:model` type, reachable or not. The library's whole annotated surface lands in `definitions`. |
| **Models, then pruned** | `ScanModels` + `PruneUnusedModels` | Discovery runs as in *Every model*, then the unreachable definitions are pruned away — you keep the reachable subset, including models discovered only because `swagger:model` published them. |

`PruneUnusedModels` is a **modifier on `ScanModels`**. Without `ScanModels` the
emitted set is already reachable-only, so the flag has nothing to do: it is a
no-op and says so with a single informational diagnostic.

## What counts as reachable

A definition survives the prune when it is reachable — directly or transitively
through any `$ref` — from one of these **roots**:

- an operation's body parameters and response schemas;
- a top-level shared `response` or `parameter`;
- a definition supplied via [`InputSpec`]({{% relref "/shaping-the-output/overlaying-a-spec" %}}).

The walk follows references through every schema shape — properties, `allOf` /
`anyOf` / `oneOf`, array items, `additionalProperties`, and so on — and
terminates cleanly on recursive or cyclic models. A model referenced **only by
another unreferenced model** is itself unreachable, so the whole dead subtree is
removed, not just its entry point.

{{% notice style="info" %}}
Definitions you supply through `InputSpec` are **pinned**: they are never pruned,
and they seed the reachability roots, so anything they `$ref` survives too. The
prune only ever removes definitions codescan *discovered*, never ones you handed
it.
{{% /notice %}}

## Pruning happens before name resolution

This is the part that makes pruning more than a convenience. codescan keys every
definition by a compiler-unique identity while it builds, then a final stage
projects each one back to the shortest unique name — deconflicting cross-package
collisions along the way (`billing.Account` / `identity.Account` →
`BillingAccount` / `IdentityAccount`; see
[Resolving $ref name conflicts]({{% relref "/shaping-the-output/resolving-name-conflicts" %}})).

`PruneUnusedModels` runs **before** that name-resolution stage. So when one half
of a colliding pair is unused, it is pruned *first* — and the collision never
happens. The surviving model keeps its clean, unqualified name instead of being
pushed to a package-qualified one to avoid a twin that is not even in your spec.
Pruning a shared library this way removes a whole class of surprising
`#/definitions/<Pkg><Name>` renames that only existed because of models you were
not using.

## Diagnostics

The prune is never silent. Through the
[`OnDiagnostic`](https://pkg.go.dev/github.com/go-openapi/codescan#Options) sink
codescan reports:

- `scan.pruned-unused` — one informational diagnostic per pruned definition,
located at the originating Go type, so you can see exactly what was dropped and
why; and the single no-op notice when the flag is set without `ScanModels`.
- `scan.renamed-definition` — one per collision the name stage *did* resolve,
located at the Go type, recording the final name it landed under. With pruning
on, collisions that vanish produce no such diagnostic at all.

## When to use it

- **Reach for `PruneUnusedModels`** when you scan a shared or third-party model
package with `-m` and want only the definitions your API actually exposes —
the reachable subset, with the noise dropped and the collision churn gone.
- **Stay on plain `ScanModels`** when the annotated package is itself the
published contract and every `swagger:model` is meant to appear.
- **Stay on the default** (neither flag) when you only ever want what your routes
reference; there is nothing extra to discover or prune.

## What's next

- [When the scanner emits a type]({{% relref "/shaping-the-output/type-discovery" %}}) —
reachability and `swagger:model`, the rules pruning builds on.
- [Resolving $ref name conflicts]({{% relref "/shaping-the-output/resolving-name-conflicts" %}}) —
the name-resolution stage pruning runs ahead of.
- [Overlaying a spec]({{% relref "/shaping-the-output/overlaying-a-spec" %}}) —
`InputSpec`, whose definitions are pinned against the prune.
3 changes: 3 additions & 0 deletions docs/doc-site/shaping-the-output/resolving-name-conflicts.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ the nested shape only when you prefer it for the over-budget tail.

- [Type discovery]({{% relref "/shaping-the-output/type-discovery" %}}) — which
types become definitions in the first place.
- [Pruning unused models]({{% relref "/shaping-the-output/pruning-unused-models" %}}) —
drops unreferenced models *before* this name stage, so collisions caused only
by models you do not use never arise.
- [Maps & free-form objects]({{% relref "/tutorials/maps-and-free-form-objects" %}}) —
the typed `additionalProperties` / `patternProperties` value forms.
- [Model definitions]({{% relref "/tutorials/model-definitions" %}}) —
Expand Down
4 changes: 3 additions & 1 deletion docs/doc-site/shaping-the-output/type-discovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ Scanned with `ScanModels: true`, the definitions are:
{{% notice style="info" %}}
If a model is missing from your spec, it is almost always **unreachable**: no
operation/parameter/response/model leads to it. Either reference it, or annotate
it `swagger:model` and scan with `ScanModels`.
it `swagger:model` and scan with `ScanModels`. For the opposite problem — a
`ScanModels` scan that pulls in models you do *not* want, like `Standalone` —
see [Pruning unused models]({{% relref "/shaping-the-output/pruning-unused-models" %}}).
{{% /notice %}}

## Generic and embedded types
Expand Down
13 changes: 13 additions & 0 deletions fixtures/enhancements/prune-unused/a/a.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

// Package a holds the referenced half of the a.Thing / b.Thing collision pair.
package a

// Thing is referenced from Used.Thing, so it survives a prune. With b.Thing
// pruned away first, this keeps the bare name "Thing".
//
// swagger:model Thing
type Thing struct {
A string `json:"a"`
}
93 changes: 93 additions & 0 deletions fixtures/enhancements/prune-unused/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

// Package pruneunused exercises Options.PruneUnusedModels.
//
// A single route references Used, which transitively reaches a chain (B -> C),
// a self-recursive model (Node), and a cross-package model (a.Thing). These are
// the reachable set and must survive a prune.
//
// Unused (a swagger:model nothing references) and OnlyByUnused (referenced only
// by Unused) must be pruned. b.Thing is a swagger:model in a sibling package
// that nothing references and must also be pruned — and because it is pruned
// BEFORE name reduction, its collision with a.Thing never materialises, so
// a.Thing keeps the bare name "Thing" instead of being deconflicted to AThing /
// BThing. That is the headline property of pruning before reduction.
package pruneunused

import (
"github.com/go-openapi/codescan/fixtures/enhancements/prune-unused/a"
"github.com/go-openapi/codescan/fixtures/enhancements/prune-unused/b"
)

// Used is referenced from the route response — the reachability root.
//
// swagger:model Used
type Used struct {
B B `json:"b"`
Thing a.Thing `json:"thing"`
Node *Node `json:"node"`
}

// B is reached via Used -> B -> C.
//
// swagger:model B
type B struct {
C C `json:"c"`
}

// C is the chain tail.
//
// swagger:model C
type C struct {
Name string `json:"name"`
}

// Node is self-recursive and reached from Used; the reachability walk must
// terminate on the cycle, not loop.
//
// swagger:model Node
type Node struct {
Next *Node `json:"next"`
}

// Unused is a swagger:model that nothing references — pruned.
//
// swagger:model Unused
type Unused struct {
Only OnlyByUnused `json:"only"`
}

// OnlyByUnused is referenced only by Unused (itself unreferenced), so it is
// reachable only through dead nodes — pruned too.
//
// swagger:model OnlyByUnused
type OnlyByUnused struct {
V string `json:"v"`
}

// usedResp carries Used in its body so the route reaches it.
//
// swagger:response usedResp
type usedResp struct {
// in: body
Body Used `json:"body"`
}

// handler is the only route.
//
// swagger:route GET /used usedOp
//
// Used.
//
// responses:
//
// 200: usedResp
func handler() {}

// keep the imported packages referenced so the tree type-checks even though
// b is only reached by the scanner, not by Go code.
var (
_ a.Thing
_ b.Thing
)
14 changes: 14 additions & 0 deletions fixtures/enhancements/prune-unused/b/b.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

// Package b holds the unreferenced half of the a.Thing / b.Thing collision
// pair. Nothing references b.Thing, so PruneUnusedModels drops it before name
// reduction — and the a.Thing / b.Thing collision never surfaces.
package b

// Thing is a swagger:model that nothing references — pruned.
//
// swagger:model Thing
type Thing struct {
B string `json:"b"`
}
84 changes: 84 additions & 0 deletions fixtures/integration/golden/enhancements_prune_unused.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{
"swagger": "2.0",
"paths": {
"/used": {
"get": {
"summary": "Used.",
"operationId": "usedOp",
"responses": {
"200": {
"$ref": "#/responses/usedResp"
}
}
}
}
},
"definitions": {
"B": {
"type": "object",
"title": "B is reached via Used -\u003e B -\u003e C.",
"properties": {
"c": {
"$ref": "#/definitions/C"
}
},
"x-go-package": "github.com/go-openapi/codescan/fixtures/enhancements/prune-unused"
},
"C": {
"type": "object",
"title": "C is the chain tail.",
"properties": {
"name": {
"type": "string",
"x-go-name": "Name"
}
},
"x-go-package": "github.com/go-openapi/codescan/fixtures/enhancements/prune-unused"
},
"Node": {
"description": "Node is self-recursive and reached from Used; the reachability walk must\nterminate on the cycle, not loop.",
"type": "object",
"properties": {
"next": {
"$ref": "#/definitions/Node"
}
},
"x-go-package": "github.com/go-openapi/codescan/fixtures/enhancements/prune-unused"
},
"Thing": {
"description": "Thing is referenced from Used.Thing, so it survives a prune. With b.Thing\npruned away first, this keeps the bare name \"Thing\".",
"type": "object",
"properties": {
"a": {
"type": "string",
"x-go-name": "A"
}
},
"x-go-package": "github.com/go-openapi/codescan/fixtures/enhancements/prune-unused/a"
},
"Used": {
"type": "object",
"title": "Used is referenced from the route response — the reachability root.",
"properties": {
"b": {
"$ref": "#/definitions/B"
},
"node": {
"$ref": "#/definitions/Node"
},
"thing": {
"$ref": "#/definitions/Thing"
}
},
"x-go-package": "github.com/go-openapi/codescan/fixtures/enhancements/prune-unused"
}
},
"responses": {
"usedResp": {
"description": "usedResp carries Used in its body so the route reaches it.",
"schema": {
"$ref": "#/definitions/Used"
}
}
}
}
Loading
Loading