vikunja/openapi-3-migration-analysi...

9.1 KiB
Raw Blame History

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/swag v1.16.6 (annotation-driven)
  • Invocation: mage generate:swagger-docsswag 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 612+ 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 via huma.NewError hook to bridge to existing models.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

  1. Author ~3000+ lines of hand-curated YAML for 161 ops × 87 models (weeks of work to reach parity)
  2. Wire generated interfaces into existing services (touches every route)
  3. Lose "code is source of truth"; gain "spec is source of truth" only if enforced by CI
  4. 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 WebHandler instances in pkg/routes/routes.go drive the bulk of the 161 endpoints via a shared Create/Read/ReadAll/Update/Delete codepath (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: govalidator via c.Validate(i) (pkg/routes/validation.go:52).
  • Errors: centralized CreateHTTPErrorHandler (pkg/routes/error_handler.go) mapping models.Err* and httpCodeGetter to HTTP codes.
  • Web framework: labstack/echo/v5 with *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 (~23 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 (~36 days) Each becomes one huma.Register call. Mechanical but per-file.

4. Validation bridge (~35 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 ~35 days
Error/auth bridge + spec wiring + tests ~3 days
Frontend SDK regeneration + smoke testing ~1 week
Total 34 weeks focused work, shippable incrementally

Risks

  • Echo v5 fork divergence — if labstack/echo/v5 changes its Context shape, 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 23 day spike:

  1. Fork humaecho for echo/v5
  2. Port one self-contained resource (e.g. Label — small surface, full CRUD)
  3. Stand up /openapi.json alongside the existing /swagger.json
  4. Verify adapter, generic-CRUD bridge, auth, and error wiring end-to-end

If the spike lands cleanly, the rest is repetition at known cost.