9.1 KiB
OpenAPI 3 Migration Analysis
Investigation of options for producing an OpenAPI 3 spec for the Vikunja API,
currently generated as Swagger 2.0 via swaggo/swag.
Current state
- Generator:
github.com/swaggo/swagv1.16.6 (annotation-driven) - Invocation:
mage generate:swagger-docs→swag init -g ./pkg/routes/routes.go --parseDependency -d . -o ./pkg/swagger - Output:
pkg/swagger/swagger.json,"swagger": "2.0" - Web framework:
labstack/echo/v5(community fork, not mainline v4) - API surface: 119 paths, 161 operations, 87 model definitions
- Spec-to-code consistency is not enforced — drift is possible and silent
Option A — Upgrade to swaggo/swag v2
swaggo has a v2 branch that adds OpenAPI 3.1 generation via a -v3.1 CLI flag.
Readiness verdict: experimental, not production-ready.
- Latest tag:
v2.0.0-rc5(2026-01-08). No stable v2.0.0 — RC line started in 2023 (~3 years in RC) - Tracking issue #548 "Proposal swag 2.0 using OpenAPI 3.0" still open, no GA timeline (178 reactions, 44 comments, last activity Dec 2025)
- PR #2156 (Mar 2026) explicitly states "v2 is out of sync with master by a large margin"
- ~11 open PRs against
base:v2, several sitting 6–12+ months - Known unfixed correctness bugs in v3 output: formData (#1554, since 2023), echo-swagger integration (#1588), oneOf/embedded-struct/binary attachment fixes unmerged
- Stable production line is still v1.16.x (Swagger 2.0 only)
Recommendation: Track, revisit at GA. Not viable today.
Option B — Convert (post-process Swagger 2.0 → OpenAPI 3)
Keep the existing swag-driven pipeline, add a conversion step.
| Tool | Output | Notes |
|---|---|---|
swagger2openapi (Mike Ralphson, Node.js) |
OAS 3.0.x | De facto standard, battle-tested |
api-spec-converter (LucyBot) |
OAS 3.0 | Older, less maintained |
gnostic (Google) |
OAS 3.0 protobuf model | Go-native, heavier |
Trade-offs
- Delivers OAS 3.0 only, not 3.1
- Some 2.0 patterns translate awkwardly (
additionalProperties: true,nullable, file uploads, security schemes) - Documentation-quality output is fine; client/server generation from converted specs sometimes hits oddities
- Does not solve the spec-to-code drift problem
Effort: ~half a day. Add Node toolchain (or Docker step) to CI.
When it wins: If downstream consumers just need OAS 3.0 for docs/SDK generation and spec fidelity isn't a correctness requirement.
Option C — Switch the code-first generator
| Tool | Approach | Echo v5 fit | Notes |
|---|---|---|---|
Huma (danielgtaylor/huma v2) |
Typed handler funcs, spec derived from input/output structs | Adapter is v4; ~1-day fork for v5 | OAS 3.1 + JSON Schema 2020-12. Most active Go-native generator. |
| Fuego | Web framework + auto-spec | Own router | Would replace Echo entirely |
| go-swagger | Annotation-driven | n/a | Still 2.0, no real OAS 3 support |
Huma details (source: github.com/danielgtaylor/huma/v2, v2.37.3 Mar 2026,
~4k stars, 1,371 commits, production-ready per README):
- Runs on top of the existing router — existing Echo middleware (JWT, CORS, rate-limit, Sentry, logger) keeps working unchanged
- Handler signature:
huma.Register(api, huma.Operation{ OperationID: "get-task", Method: "GET", Path: "/tasks/{id}", }, func(ctx context.Context, in *struct{ ID int `path:"id"` }) (*struct{ Body Task }, error) { ... }) - Validation automatic from struct tags (
minLength,pattern,enum,format,required...); business-rule validation stays in models/services - Security schemes declared in
huma.Config, referenced per-operation; Huma does not verify tokens (existing JWT middleware stays authoritative) - Errors use RFC 9457
application/problem+json; customizable viahuma.NewErrorhook to bridge to existingmodels.Err*types - Incremental migration supported: same
*echo.Echo, two specs served in parallel, migrate endpoint-by-endpoint
Option D — Spec-first / inverted
Hand-write openapi.yaml, generate handlers from it.
| Tool | What it generates | Server frameworks |
|---|---|---|
oapi-codegen |
Types + server interface + clients | Echo v4, Chi, Gin, Fiber, net/http (no v5) |
ogen (ogen-go/ogen) |
Strict types + own radix router + clients | None — replaces router |
| OpenAPI Generator (openapitools) | Stubs + clients (many languages) | Go template, not idiomatic |
Cost for vikunja
- Author ~3000+ lines of hand-curated YAML for 161 ops × 87 models (weeks of work to reach parity)
- Wire generated interfaces into existing services (touches every route)
- Lose "code is source of truth"; gain "spec is source of truth" only if enforced by CI
- Echo v5 friction for
oapi-codegen; ogen drops Echo entirely
When it wins: If you also want multi-language SDKs, server-side validation as a primary feature, and can absorb several weeks of upfront investment.
Recommendation: Huma (Option C)
Best balance of:
- OAS 3.1 output (not just 3.0)
- Compile-time guarantee that handlers match spec (solves drift)
- Preserves existing Echo + middleware + permissions investment
- Supports incremental migration with zero big-bang risk
ogen is technically excellent but the wrong shape — it requires abandoning Echo and writing the spec first, which conflicts with vikunja's existing architecture.
Huma migration plan
Vikunja-side ground truth
- 31 generic
WebHandlerinstances inpkg/routes/routes.godrive the bulk of the 161 endpoints via a sharedCreate/Read/ReadAll/Update/Deletecodepath (pkg/web/handler/*.go) with a struct-factory and central permissions/validation. Re-implementing this generic layer once in Huma auto-covers most CRUD endpoints. - ~30 custom handler files in
pkg/routes/api/v1/(login, OAuth, OIDC, TOTP, attachments, exports, webhooks, CalDAV) — per-endpoint rewrite work. - Validation:
govalidatorviac.Validate(i)(pkg/routes/validation.go:52). - Errors: centralized
CreateHTTPErrorHandler(pkg/routes/error_handler.go) mappingmodels.Err*andhttpCodeGetterto HTTP codes. - Web framework:
labstack/echo/v5with*echo.Context(pointer).
What the migration touches
1. Echo v5 adapter (~1 day)
Huma ships humaecho for labstack/echo/v4 only (~150 LOC wrapping
echo.Context). Fork it for v5: swap the import, adjust for the pointer
Context shape, write tests. Community precedents exist (superstas/huma,
eugenepentland/huma).
2. Generic WebHandler rewrite (~2–3 days)
The leverage point. Re-express pkg/web/handler/{create,read,readall,update,delete}.go
using Huma generics so all 31 existing generic handlers migrate in one pass.
3. Custom handlers (~3–6 days)
Each becomes one huma.Register call. Mechanical but per-file.
4. Validation bridge (~3–5 days) Two paths:
- Migrate tags: convert
valid:"required,email"→validate:"required" format:"email". Tedious but a clean win — constraints become visible in the spec. - Keep govalidator: Huma middleware calls the existing validator after tag validation. Cheaper, but constraints stay invisible in the spec.
5. Error mapping (~1 day)
Wrap existing models.Err* types via huma.NewError so
httpCodeGetter + RFC 9457 application/problem+json produce matching
responses.
6. Auth declaration (~0.5 day)
JWT middleware stays on Echo. Declare bearer scheme once in
huma.Config.Components.SecuritySchemes; reference per-operation via
Operation.Security. Permissions logic in models stays exactly as-is.
Incremental rollout
Same *echo.Echo, two specs served in parallel during transition:
/api/v1/<legacy> ← swag-annotated handlers, /swagger.json (OAS 2.0)
/api/v1/<migrated> ← Huma handlers, /openapi.json (OAS 3.1)
Migrate one resource at a time, ship continuously. check:got-swag stays
valid for unmigrated routes; add check:openapi for the Huma portion. When
the last endpoint is moved, retire swag.
Budget
| Phase | Effort |
|---|---|
| Adapter fork + generic CRUD shell | ~1 week |
| Custom handlers | ~1 week (parallelizable) |
| Validation tag migration | ~3–5 days |
| Error/auth bridge + spec wiring + tests | ~3 days |
| Frontend SDK regeneration + smoke testing | ~1 week |
| Total | 3–4 weeks focused work, shippable incrementally |
Risks
- Echo v5 fork divergence — if labstack/echo/v5 changes its
Contextshape, the adapter needs updating. Surface is small, low risk. - Frontend impact — JSON shapes stay compatible (same tags), but error JSON changes to RFC 9457. Plan one round of frontend service updates.
- Two-spec window — needs documenting for API consumers. The only realistic alternative is a multi-month freeze.
Suggested first step: spike
Before committing to the full migration, run a 2–3 day spike:
- Fork
humaechoforecho/v5 - Port one self-contained resource (e.g.
Label— small surface, full CRUD) - Stand up
/openapi.jsonalongside the existing/swagger.json - Verify adapter, generic-CRUD bridge, auth, and error wiring end-to-end
If the spike lands cleanly, the rest is repetition at known cost.