---
name: Adopt The Huma OpenAPI Wrapper
slug: adopt-the-huma-openapi-wrapper
category: AI Engineering
description: Adopt The Huma OpenAPI Wrapper wires a Go service to the shared Huma OpenAPI 3.1 and RFC 9457 problem packages. Use it when migrating or standardizing an HTTP API on the lib-commons wrapper and its spec generation and error handling patterns.
github: "https://github.com/LerianStudio/ring/tree/main/dev-team/skills/adopting-lib-commons-huma-wrapper"
language: HTML
stars: 210
forks: 27
install: "npx degit https://github.com/LerianStudio/ring/tree/main/dev-team/skills/adopting-lib-commons-huma-wrapper ~/.claude/skills/adopting-lib-commons-huma-wrapper"
installs_to: ~/.claude/skills/adopting-lib-commons-huma-wrapper
source_path: dev-team/skills/adopting-lib-commons-huma-wrapper/SKILL.md
collection_size: 25
category_size: 2970
collection_url: "https://dirskills.com/collections/LerianStudio/ring"
added: 2026-09-04T05:26:33.014Z
last_synced: 2026-09-04T05:26:33.014Z
canonical_url: "https://dirskills.com/skills/adopt-the-huma-openapi-wrapper"
---

# Adopt The Huma OpenAPI Wrapper

Adopt The Huma OpenAPI Wrapper wires a Go service to the shared Huma OpenAPI 3.1 and RFC 9457 problem packages. Use it when migrating or standardizing an HTTP API on the lib-commons wrapper and its spec generation and error handling patterns.

**Install:**

```bash
npx degit https://github.com/LerianStudio/ring/tree/main/dev-team/skills/adopting-lib-commons-huma-wrapper ~/.claude/skills/adopting-lib-commons-huma-wrapper
```

## README

# Adopt the lib-commons Huma OpenAPI Wrapper

## When to use
- A Go service needs code-first OpenAPI 3.1 over Fiber (Huma) with RFC 9457 `application/problem+json` errors
- User asks to "adopt the lib-commons OpenAPI/problem wrapper", "use the shared Huma wrapper", "standardize error responses on RFC 9457"
- A service already carries a **local copy** of the wrapper (`internal/shared/adapters/openapi`, `shared/openapi`, `shared/humaerr`, a hand-rolled `huma.NewError` override) and should move onto the shared `lib-commons/v5/commons/net/http/{openapi,problem}`
- A service is still on swaggo/swag (Swagger 2.0) and is migrating to Huma

## Skip when
- Service is not a Go project
- Task does not involve an HTTP API / OpenAPI spec
- The service genuinely needs a bespoke error envelope that is NOT RFC 9457 (rare; challenge it first)
- Documentation-only or non-code task

You orchestrate. Agents implement. NEVER use Edit/Write/Bash on Go source files — all code changes go through `Task(subagent_type="ring:backend-go")`. TDD mandatory for implementation gates (RED → GREEN → REFACTOR). The orchestrator owns git (commit/push/PR/release) and NEVER commits without the user's go-ahead.

## Architecture

The wrapper is **two packages** in `lib-commons/v5` (minimum **v5.6.0**):

| Alias | Import Path | Purpose |
|-------|-------------|---------|
| `openapi` | `github.com/LerianStudio/lib-commons/v5/commons/net/http/openapi` | `Config`, `New` (builds the `humafiber` API), `DeclareBearerAuth`, `ServeSpec` |
| `problem` | `github.com/LerianStudio/lib-commons/v5/commons/net/http/problem` | `Install` (global `huma.NewError` override), `MapError`, `Detail`, `BaseURI` |

**`openapi.New(app, group, cfg)`** constructs the Fiber-v2 Huma API via `humafiber.NewV2WithGroup` (the default `humafiber.New` targets Fiber v3 — wrong major), strips Transformers/OnAddOperation/CreateHooks, and **clears the auto-mounted `/openapi.json` + `/docs`** (auto-mount is a latent production exposure — the spec must be served explicitly and gated). `Config{Title, Version, Description, Servers}`.

**`openapi.ServeSpec(app, api, logger, prefix, title)`** mounts `{prefix}/openapi.json`, `{prefix}/openapi.yaml`, `{prefix}/docs` (Scalar UI). It normalizes `prefix` (leading slash, no trailing) and HTML-escapes the docs-page title and spec URL. **Gate this behind the service's `Swagger.Enabled`** flag — never serve unconditionally.

**`openapi.DeclareBearerAuth(api)`** registers the `BearerAuth` security scheme *component* (`components.securitySchemes`) so `Security` references resolve — it does NOT attach the requirement to anything. **The scheme component alone advertises ZERO secured operations.** To make the spec say "auth required" you MUST also attach a requirement, one of:
- **Global default** (covers every op in one line, drift-safe): `api.OpenAPI().Security = []map[string][]string{{"BearerAuth": {}}}` right after `openapi.New`.
- **Per-operation**: `Security: []map[string][]string{{"BearerAuth": {}}}` on each `huma.Operation`.

`DeclareBearerAuth` without a requirement is the classic regression: a JWT-enforced API whose spec advertises every endpoint as public. **Public endpoints** (no auth middleware at runtime) must override the global default with an explicit empty requirement: `Security: []map[string][]string{}` on the operation — a *non-nil empty slice*, which Huma renders as `security: []` because `Operation.Security` marshals with `omitNil` (Go `json:"...,omitempty"` would drop it and silently re-inherit the global default). Getting this wrong makes the spec lie about which routes need a token — see also the constraint-as-validation caveat under Dependency facts.

**`problem.Install()`** is the crux. It is a `sync.Once` override of the process-global `huma.NewError`, so EVERY error Huma builds — domain errors via `MapError` and the framework's own validation/404/422 errors — becomes a `*problem.Detail`. Merge semantics:
- **status >= 500**: body scrubbed to the static `"internal error"`, NO `errs` folded. This is the central info-leak guard — even a careless `huma.Error500(rawErr.Error())` cannot leak an internal cause.
- **status < 500**: `msg` passes through and `errs` fold into `Errors[]` in order (skip nil, honor `huma.ErrorDetailer`) — native 422 field errors are preserved.

**`problem.MapError(err, codeOf, statusOf, fallbackCode)`** is the per-rail **flex seam** — the one place each service injects its own taxonomy:
- `codeOf func(error) (code, msg string, ok bool)` — extract a domain code; `ok=false` → unrecognized → sanitized 500 carrying `fallbackCode`.
- `statusOf func(code string) int` — map code → HTTP status.
- `fallbackCode` — code carried when the error is nil/unrecognized.
- A service WITH a code taxonomy passes real callbacks + a fallback (e.g. `"SPB-9000"`); a **bare** service passes an empty fallback and `code` is dropped (`omitempty`).

**`problem.Detail`** embeds `huma.ErrorModel` + an `omitempty` `Code`. Services that never set a code emit a bare RFC 9457 body; coded errors mapped through `MapError` carry the machine code in `Code` + a flat `Type` URI (`BaseURI + "/" + code`). `Type` is set **only** on the `MapError` coded path — framework-built errors (native 422/404 via `Install`) keep `Type` at `about:blank` with empty `Code`.

## Dependency facts (do not re-litigate)

- **huma version MUST match** what the pinned lib-commons build uses (`danielgtaylor/huma/v2 v2.38.0` at v5.6.0). A mismatch is module-graph skew that the security-scan/SBOM gate will flag.
- **`humafiber` drags `gofiber/fiber/v3` as an indirect dep** — this is EXPECTED, not a bug. `humafiber` is one package importing both Fiber majors; Go compiles the whole package, so importing it for the v2 funcs pulls v3 into the graph. Only the v2 path compiles into the binary. `govulncheck` stays quiet (v3 unreachable); Trivy/SBOM may flag *future* fiber/v3 CVEs — a waivable false positive, NOT a reason to avoid the wrapper.
- Test the API through the real `*fiber.App` via `app.Test`, not `humatest` (the humatest adapter diverges on `Unwrap`).
- **Huma schema-constraint struct tags are VALIDATION, not documentation.** `minLength`/`maxLength`/`minItems`/`maxItems`/`minimum`/`maximum`/`pattern`/`enum` are enforced by Huma at the framework layer — a violating request is rejected with a 422 *before* the handler runs. Adding one to "make the spec match the runtime limit" silently relocates rejection from the handler (its domain `400`/coded error) to Huma's `422`, changing the error contract and making the handler's coded path unreachable for that field. The runtime limit is usually already enforced by a `validate:` tag (go-playground); duplicating it as a Huma constraint moves the enforcement layer. Treat constraint tags as a behavioral change (needs error-contract + test updates), never a doc-only edit. By contrast `doc:`/`example:` tags ARE pure documentation and safe to add freely. Decisive test: if a unit test's expected status flips, it's behavioral.

**Mandatory agent instruction (include in EVERY dispatch):**

> Use the canonical imports `github.com/LerianStudio/lib-commons/v5/commons/net/http/openapi` and `.../problem`. Do NOT reintroduce a local openapi/humaerr wrapper or a hand-rolled `huma.NewError` override — `problem.Install()` owns that globally.
> `problem.Install()` MUST be called before ANY Huma operation is registered, on BOTH the runtime API and any spec-gen API, or the runtime error bodies and the committed spec diverge.
> Match the huma version the pinned lib-commons uses (v2.38.0 @ v5.6.0). The indirect `fiber/v3` is expected — do not try to remove it.
> Serve the spec only via `openapi.ServeSpec`, gated on the Swagger-enabled flag. Never rely on Huma auto-mount.
> If the API uses bearer auth, `DeclareBearerAuth` alone advertises nothing — attach a security requirement (global `api.OpenAPI().Security` default or per-op) and override genuinely-public ops with `Security: []map[string][]string{}`.
> Make the generated spec rich, not hollow: `doc:` + `example:` tags on request/response DTO fields, and `info.Contact`/`info.License` + top-level `Tags` (with descriptions) on the document. Do NOT add `minLength`/`maxLength`/`maxItems` etc. as a doc gesture — those are enforced validation (see Dependency facts).
> Preserve the service's existing per-rail code taxonomy verbatim — only rewire the wrapper call into `problem.MapError`.
> TDD: RED → GREEN → REFACTOR for every gate. Test via `app.Test`, not humatest.

## Gate Overview

| Gate | Name | Condition | Agent |
|------|------|-----------|-------|
| 0 | Stack Detection + Compliance Audit | Always | Orchestrator |
| 1 | Codebase Analysis (HTTP + error focus) | Always | ring:codebase-explorer |
| 1.5 | Implementation Preview | Always | ring:visualizing |
| 2 | lib-commons v5.6.0+ + huma version alignment | Always | ring:backend-go |
| 3 | `openapi.New` + `problem.Install` on BOTH paths | Always | ring:backend-go |
| 4 | Per-rail `problem.MapError` wiring | Skip only if zero domain-error mapping (bare framework errors only) | ring:backend-go |
| 5 | `ServeSpec` (gated) + bearer auth (scheme + requirement + public overrides) | Always | ring:backend-go |
| 5.5 | Spec richness + document metadata (`doc:`/`example:`, contact/license/tags) | If the API advertises a spec to clients | ring:backend-go |
| 6 | Delete the local wrapper | Migration mode only (skip for greenfield) | ring:backend-go |
| 7 | Regenerate spec + prove rename-only | Always | ring:backend-go |
| 8 | Tests (build + unit `-race` + spec-drift gate) | Always | ring:backend-go |
| 9 | Code Review | Always | 9 defaults + triggered specialists |
| 10 | User Validation | Always | User |
| 11 | Release/pin discipline (cross-repo) | Only if the wrapper version is unreleased | Orchestrator + User |

Gates execute sequentially. Any surviving local wrapper or hand-rolled `huma.NewError` override = NON-COMPLIANT = gates cannot be skipped.

## Gate 0: Stack Detection + Compliance Audit

Orchestrator executes directly.

```bash
grep -n "lib-commons/v5" go.mod                                  # version (need >= v5.6.0)
grep -n "danielgtaylor/huma" go.mod                              # huma version (match the lib build)
grep -rn "humafiber\|huma.Register\|huma.Get\|huma.Post" internal/ cmd/   # existing Huma usage
grep -rln "huma.NewError" internal/ cmd/                         # hand-rolled overrides (must go)
grep -rn "shared/openapi\|shared/humaerr\|adapters/openapi" --include='*.go' .  # LOCAL wrapper (migration mode)
grep -rn "swaggo\|gofiber/swagger\|// @" --include='*.go' . docs/    # legacy swaggo (migrating from 2.0)
grep -rn "ServeSpec\|openapi.New\|problem.Install" internal/ cmd/  # already partially adopted?
grep -rn "Swagger.Enabled\|SWAGGER_ENABLED" internal/            # the serving gate flag
```

**Mode detection:**
- **Migration mode** — a local `openapi`/`humaerr` package or a hand-rolled `huma.NewError` override exists → Gate 6 runs.
- **Greenfield mode** — Huma not yet wired (or swaggo only) → Gate 6 skipped, Gate 3 builds fresh.

**Compliance audit** (if any Huma code detected):
- `problem.Install()` present on the runtime path AND the spec-gen path
- No surviving local wrapper / hand-rolled `huma.NewError` override
- Spec served only via `ServeSpec`, gated; no auto-mount
- huma version matches the lib-commons build

## Gate 2: Version + huma alignment

Ensure `go.mod` requires `lib-commons/v5 >= v5.6.0` and `danielgtaylor/huma/v2` at the version that lib-commons build uses. `go mod tidy`; expect `fiber/v3` to appear/stay indirect (expected). If validating against an **unreleased** wrapper version, use a local `replace => ../lib-commons` (see Gate 11) — never pin a pre-release for merge.

## Gate 3: `openapi.New` + `problem.Install` — BOTH paths

The single most common defect is wiring `problem.Install()` on only one path. Huma resolves the error response schema at **operation-registration time**, and `Install` swaps a process-global, so it must run **before** every `huma.Register/Get/Post` on:
1. the **runtime** API (bootstrap/routes), and
2. every **spec-gen** entrypoint (the `cmd/huma-spec`-style builder that writes the committed `openapi.yaml`).

If the two disagree on whether `Install` ran, the served error bodies and the committed spec diverge. `openapi.New` itself registers no operations (auto-mount cleared), so the correct order is: `api := openapi.New(...)` → `problem.Install()` → register operations.

## Gate 4: Per-rail `problem.MapError`

Replace the service's existing domain-error→HTTP translation with `problem.MapError`, **preserving its code taxonomy verbatim**. Wire `codeOf`/`statusOf`/`fallbackCode`:
- Coded rail: `codeOf` reads the service's domain-error code, `statusOf` is its code→status table, `fallbackCode` is the house "unknown" code.
- Bare rail: empty `fallbackCode`; `Code` is omitted from the body.
Verify the mapped status semantics are unchanged from before (this gate is a rewire, not a re-design).

## Gate 5: `ServeSpec` (gated) + bearer auth (scheme + requirement + public overrides)

Mount the spec via `openapi.ServeSpec(app, api, logger, prefix, title)` behind the Swagger-enabled flag. Confirm the served paths match the service's existing convention (no path drift).

If the API uses bearer auth, wiring it correctly is THREE steps, not one:
1. `openapi.DeclareBearerAuth(api)` before registration — registers the scheme *component*.
2. Attach the requirement, or the spec advertises every op as public: a **global default** `api.OpenAPI().Security = []map[string][]string{{"BearerAuth": {}}}` (one line, covers all ops, drift-safe) or **per-operation** `Security` on each `huma.Operation`.
3. **Override genuinely-public ops** (no auth middleware at runtime) with `Security: []map[string][]string{}` (non-nil empty → Huma renders `security: []`). Skipping this makes the spec claim a token is required where it isn't.

Cross-check the spec's security against the real middleware chains: every op the spec marks secured must actually enforce auth at runtime, and every public route must carry the empty override. A spec that disagrees with the middleware is a contract bug, not cosmetics.

## Gate 5.5: Spec richness + document metadata

Only when the service advertises a spec to clients. A spec that lints but carries hollow schemas is low-value. Ensure:
- **Field documentation:** `doc:` and `example:` tags on request/response DTO fields (the leaf structs Huma reflects — for a `Body <Type>` marker the leaf lives in the sibling DTO file, not the `*_huma.go` wrapper; only structs reachable from a registered `huma.Register` Input/Output or `r.Schema(reflect.TypeOf(...))` are reflected, so tags on inert internal structs do nothing — confirm each example appears in the regenerated spec).
- **Document metadata:** `info.Contact`, `info.License`, document `Servers` (via `openapi.Config.Servers`), and a top-level `Tags` array whose entries carry descriptions covering every operation-tag string in use. Contact/License/Tags are set post-`New` (`api.OpenAPI().Info`/`.Tags`); like every spec mutation they must be applied identically on BOTH the runtime and spec-gen assembly paths if the service has two (the drift/byte-identity gate enforces this).
- **Do NOT** reach for `minLength`/`maxLength`/`maxItems` to "enrich" — those are enforced validation, not docs (see Dependency facts). `doc:`/`example:` are the documentation surface.

## Gate 6: Delete the local wrapper (migration mode)

Remove the local `openapi`/`humaerr` packages and any hand-rolled `huma.NewError` override. Then:
- `grep -rn` for the old import paths/symbols → must be EMPTY in Go code.
- Relocate any cross-package parity/contract test out of the deleted dirs; retarget renamed schema component names.
- Fix stale doc-comments AND live docs (READMEs/architecture docs) that point at the removed packages — the diff-stat won't show these; grep for them.

## Gate 7: Regenerate spec + prove rename-only

Regenerate the committed OpenAPI spec(s). The diff should be **only** the error-schema rename (`ErrorModel`→`Detail`, or a local `ProblemDetail`→`Detail`) + the additive `code` property + `$ref` re-targets. **Do not trust the diff-stat** — an alphabetical component re-sort inflates insert/delete counts. Prove rename-only with an order-independent comparison:

```bash
diff <(git show HEAD:path/to/openapi.yaml | sed 's/ProblemDetail/Detail/g' | sort) <(sort path/to/openapi.yaml)
# EMPTY => pure rename (HEAD+rename equals worktree as a line multiset)
```

Confirm/refresh the committed-spec drift gate so a stale spec fails CI.

## Gate 8: Tests

Build + unit `-race` + the spec-drift gate. Error-policy assertions (the highest-value tests in the migration): a forced `>=500` asserts `detail == "internal error"` AND `Errors == nil` (no leaked cause); a `<500` validation error keeps its `Errors[]` field list. Note: `huma.ErrorModel.Type` is `omitempty` and is set only on the `MapError` coded path — assert `type` only on coded-domain responses, never on native 422/404 (where it stays `about:blank`/absent). Always assert `title`/`status`/`detail`/`code`.

## Gate 11: Release / pin discipline (cross-repo, only if the wrapper version is unreleased)

Relevant only when this adoption rides an **unreleased** lib-commons wrapper change (you modified the wrapper too). The shared `security-scan` gate **reds any PR pinned to a pre-release version**. So:
1. Validate the consumer against the beta via a local `replace => path/to/lib-commons` (the require can lag; replace overrides resolution).
2. Cut the lib-commons **stable** tag (develop→main release PR; semantic-release auto-tags) BEFORE pinning any consumer.
3. Drop the `replace`, bump `require` to the stable tag (a stale `require` masked by a replace will break the build when the replace is removed — bump them together), `go mod tidy && go mod verify`.
4. Only then open/merge the consumer PR — now the pre-release pin gate passes.
Intra-repo `replace` directives (module wiring within a multi-module repo) are fine and version-scoped; the gate targets pre-release *versions*, not the existence of a `replace`.

## Severity Reference

| Severity | Criteria |
|----------|----------|
| CRITICAL | `problem.Install()` missing on the runtime OR spec-gen path (runtime/spec divergence); a surviving hand-rolled `huma.NewError` override or local wrapper still in the binary; a `>=500` body leaking a raw cause (bypassing the central scrub) |
| HIGH | Per-rail `MapError` not wired (errors not RFC 9457); local wrapper not fully deleted (dangling refs / dead code); committed spec not regenerated (drift gate red); a consumer PR pinned to a pre-release lib-commons version (merge gate red); `DeclareBearerAuth` wired but NO security requirement attached (global or per-op) — the spec advertises every operation as unauthenticated while the runtime enforces JWT (the spec lies about the security contract); a genuinely-public route missing its `Security: []map[string][]string{}` override (spec demands a token the route doesn't); a Huma schema-constraint tag (`minLength`/`maxLength`/`maxItems`…) added as a doc gesture, silently moving rejection from the handler's `400` to Huma's `422` (error-contract change) |
| MEDIUM | huma version skew vs the lib-commons build (graph/SBOM flag); `ServeSpec` not gated on the Swagger flag (prod spec exposure); treating the indirect `fiber/v3` as a bug to remove; a client-facing spec with hollow schemas — DTO fields lacking `doc:`/`example:` — or missing document metadata (contact / license / top-level tags with descriptions) |
| LOW | `DeclareBearerAuth` omitted entirely on a bearer API (scheme undocumented — less harmful than a scheme with no requirement, which actively misleads); asserting on the absent `type` field; spec `Servers` left unset when the service should advertise one |

## Related

**Complementary:** ring:using-lib-commons (the broader non-o
