Code Generation
The two-pass codegen pipeline — the legacy Internal API and the Official OpenAPI surface — and the two independent version axes that pin them.
A single go generate produces all of go-unifi's *.generated.go files. Under the hood it runs two
passes over two independent inputs, pinned by two version markers. This page explains the moving
parts; for the exact commands to run, see Regenerating.
One command, two passes
The root generator (codegen/main.go, package main) orchestrates everything. Its generate() function
resolves versions, makes the inputs available, runs both passes, and writes the version artifacts.
Internal pass — codegen/internal (internal.Generate). Reads the controller's field-definition JSONs
and emits unifi/<resource>.generated.go (resource structs + private CRUD) and unifi/client.generated.go
(the Client/InternalClient interfaces). Hand-maintained v2/ definitions render through a dedicated
template.
Official pass — codegen/official (runOfficialPass). Reads the committed OpenAPI snapshot and emits the
whole unifi/official/ surface: the oapi-codegen models, the per-tag fluent wrappers, the Client interface,
and the per-group mocks.
Version artifacts — root writes unifi/version.generated.go plus both markers (.unifi-version and
.unifi-version-official) so the generated code and the markers always move together.
Why the Official pass is a separate module
codegen/official carries its own go.mod. The root generator shells out to it as a subprocess
(go run .) rather than importing it. That keeps the heavy oapi-codegen / kin-openapi toolchain out of the
published go.mod graph — consumers of the library never pull those in. The subprocess runs with
GOFLAGS=-buildvcs=false and reads the committed spec offline.
codegen/main.go (package main, root module)
├── codegen/internal Internal-API pass (in-process)
│ └── unifi/*.generated.go, unifi/client.generated.go
└── codegen/official Official-API pass (separate go.mod, shelled out via `go run .`)
└── unifi/official/*.generated.goTwo inputs, both committed snapshots
Both passes read from committed snapshots, so generation is deterministic and fully offline by default. No controller download is needed for a normal regenerate.
| Pass | Input | Committed location |
|---|---|---|
| Internal | Controller field-definition JSONs (extracted from the UniFi Network Application package) | codegen/v<version>/ (e.g. codegen/v9.5.21/) |
| Official | UniFi OpenAPI (integration/v1) spec | codegen/openapi/integration-<version>.json |
The Internal field snapshot is frozen at codegen/v9.5.21/ (committed, with a matching .gitignore
un-ignore rule). Because the inputs are committed, the daily regeneration CI is a deterministic no-op unless a
snapshot or template actually changes.
Multiple Official snapshots may coexist under codegen/openapi/: bumping the spec keeps the older
integration-<ver>.json alongside the new one. The Official pass generates the Go surface from the pinned
version (the positional argument / .unifi-version-official); the docs website always renders the
newest committed snapshot by version order. If the pinned snapshot already exists, the generator uses it
offline and skips the download.
If a committed snapshot is missing, the generator falls back to downloading it. download.go is the only
remote-ingest point — HTTPS with Ubiquiti-host pinning, atomic extraction, and size caps. A package that
predates the Official API (no integration.json) is skipped with a warning so the Internal pass never
regresses.
Two version axes
The Internal and Official surfaces version independently. Each has its own marker file at the repo root and
its own resolver in codegen/version.go.
Prop
Type
The Internal axis is capped at 9.5.21
9.5.21 is the last UniFi Network Application release that ships the internal api/fields/*.json
definitions. Newer Network Application packages (10.x and later) dropped those files, so there is nothing to
generate Internal resources from beyond this point — the legacy field-definition format is end-of-life there.
(9.5.21 and 10.x are Network Application versions, unrelated to the console's UniFi OS version.)
resolveInternalVersion enforces the cap:
latestresolves the newest release, then clamps tomin(resolved, 9.5.21).- An explicit version
≤ 9.5.21is used as-is. - An explicit version
> 9.5.21fails loudly, pointing you at the Official OpenAPI surface instead.
The Official axis floors at 10.1.78
10.1.78 is the first controller whose package ships integration.json (the Official OpenAPI spec).
resolveOfficialSpecVersion decouples this from the Internal version: when set explicitly (the positional
argument) it uses exactly that version; otherwise it reuses the Internal version if it is ≥ 10.1.78, and
falls back to the latest release when the Internal version predates the Official API.
The committed go generate directive pins both axes explicitly, so the canonical regenerate is fully
reproducible. The positional argument is the Official spec version; -legacy-version pins the Internal
controller version:
//go:generate go run ../codegen/ -version-base-dir=../codegen/ -legacy-version=9.5.21 10.1.85Generator flags
The root generator (go run ./codegen [OPTIONS] [official-spec-version]) accepts:
| Flag | Description | Default |
|---|---|---|
-version-base-dir | Base directory for version JSON files (and the openapi/ snapshot dir) | . |
-output-dir | Output directory for the generated Go code | . |
-legacy-version | Pin the Internal/legacy controller version (capped at 9.5.21) | latest |
-download-only | Only fetch/build the input JSONs; do not generate | false |
-debug | Enable debug logging | false |
-trace | Enable trace logging (takes precedence over -debug) | false |
The positional [official-spec-version] argument pins the Official OpenAPI spec version (e.g. 10.1.85); leave
it empty to auto-select (the Internal version when ≥ 10.1.78, otherwise the latest release).
What guards it
CI re-runs generation and fails if the working tree is not byte-identical:
test-codegenrunsgo generate unifi/codegen.go, thengit diff --exit-code.test-codegen-officialruns thecodegen/officialmodule's own test suite.- The Official pass is additionally guarded by determinism / committed-surface tests in the
codegen/officialmodule (TestSurfaceMatchesCommitted,TestSurfaceDeterministic). - The generated models carry a round-trip test,
models_roundtrip_test.go, which lives in the generatedunifi/officialpackage (not thecodegen/officialmodule) and runs under the normalgo test ./...suite.
See also
Architecture
How go-unifi is built — generated resource code vs. the hand-written client layer, the Client interface, and the one-way unifi → official dependency.
Customizations
Steer the Internal-API code generator with customizations.yml — field type/name/tag overrides, exclude predicates, custom client functions, and JSON unmarshalers.