docs: add OpenAPI 3 migration analysis

Investigation of options for producing an OpenAPI 3 spec: swag v2 status,
converter post-processing, switching to Huma, and spec-first with ogen.
Recommends Huma with an Echo v5 adapter fork and incremental rollout.
This commit is contained in:
Claude 2026-04-20 07:28:07 +00:00
parent 56aef03697
commit be5d307813
No known key found for this signature in database
1 changed files with 217 additions and 0 deletions

View File

@ -0,0 +1,217 @@
# 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-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 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:
```go
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.