RFC: OpenAPI Generation from Go Types
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 5/5
- Tempo stimato
- Più di una settimana
- Idoneità per principianti
- 15/100
- Tipo di issue
- Funzionalità
- Chiarezza
- Da chiarire
- Stato di attività
- Ferma
- Stack tecnologico
- go
- Ambito
- backend-api-design
Direzione di ricerca
Start by reading this RFC and inspecting the existing route definitions and docs/apis/openapi.yaml; the issue does not name the route-definition file or tests. The proposal depends on an auth refactor and leaves polymorphic destination schemas open. Clarify the design and scope with maintainers before implementation; the issue does not define a concrete completion criterion.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
OpenAPI Generation
Problem
The OpenAPI schema (docs/apis/openapi.yaml, 4,053 lines) is manually maintained. This creates:
- Drift risk - Schema can get out of sync with actual API behavior
- Duplication - Request/response types defined in both Go and YAML
- Manual updates - Every API change requires updating the schema
Goals
- Single source of truth - Go code defines the API, OpenAPI is derived
- No build step - Spec generated at runtime, always in sync
- Type-safe - Errors, inputs, outputs are Go types, not strings
- Live endpoint - Serve at
/api/v1/openapi.json
Prior Art: swaggo/swag
swaggo/swag is the most popular Go OpenAPI generator. It uses comment annotations:
// CreateDestination godoc
// @Summary Create a new destination
// @Description Creates a destination for receiving webhooks
// @Tags Destinations
// @Accept json
// @Produce json
// @Param tenantID path string true "Tenant ID"
// @Param body body CreateDestinationRequest true "Destination configuration"
// @Success 201 {object} DestinationDisplay
// @Failure 400 {object} BadRequestError
// @Failure 409 {object} ConflictError
// @Router /tenants/{tenantID}/destinations [post]
func (h *Handler) CreateDestination(c *gin.Context) {
// ...
}
Then generate with:
swag init -g cmd/main.go -o docs/
I like this approach - it's battle-tested, widely adopted, and has great Gin integration.
However, I decided against it for two reasons:
-
Comments are not type-safe - The annotations are strings that can drift from the actual code. Nothing prevents
@Success 201 {object} WrongTypefrom compiling. -
Requires a build step - Running
swag initbefore every build adds friction. The generated spec can get stale if someone forgets to regenerate.
We want our OpenAPI spec to be derived directly from Go types at runtime, with no opportunity for drift.
Design
Our approach is broken into two distinct scopes:
Scope 1: OpenAPI Metadata in RouteDefinition
Add an OpenAPI field to the existing RouteDefinition struct. This allows us to generate OpenAPI schema from our current route definitions without refactoring handlers.
type RouteDefinition struct {
Method string
Path string
Handler gin.HandlerFunc
AuthMode AuthMode
TenantScoped bool
OpenAPI *OpenAPIMeta // NEW: optional OpenAPI metadata
}
type OpenAPIMeta struct {
Summary string
Description string
Tags []string
OperationID string
RequestBody reflect.Type // e.g., reflect.TypeOf(CreateDestinationRequest{})
Response reflect.Type // e.g., reflect.TypeOf(DestinationDisplay{})
Errors []ErrorMeta // e.g., {Status: 409, Type: reflect.TypeOf(ConflictError{})}
}
Scope 2: Generic Operation with Typed Input/Output/Errors
Refactor handlers to use a generic Operation type that replaces RouteDefinition. This encodes the full API contract in the type system, providing both type-safe code AND accurate OpenAPI generation.
Instead of registering handlers directly, define typed operations:
type Operation[In, Out any, Err ErrorSet] struct {
Method string
Path string
Auth AuthMode // Public, Tenant, Admin
TenantScoped bool // Contract: handler receives *Tenant
OpenAPI OpenAPIMeta
Handler func(ctx context.Context, input In) (*Out, Err)
}
The type parameters make the contract explicit:
In- Input type (path params, query params, request body)Out- Success response type (implementsHTTPStatus()for non-200)Err- Possible error types (as a struct with pointer fields)
Response Wrappers
Generic wrapper types modify response behavior (status codes, pagination). The OpenAPI generator reflects on Out to extract the inner type for the schema.
Status Codes
By default, success responses return 200. For other status codes, the Out type implements HTTPStatus():
type Created[T any] struct{ *T }
func (Created[T]) HTTPStatus() int { return 201 }
type Accepted[T any] struct{ *T }
func (Accepted[T]) HTTPStatus() int { return 202 }
type NoContent struct{}
func (NoContent) HTTPStatus() int { return 204 }
Usage:
func (h *Handlers) Get(...) (*DestinationDisplay, GetErrors) // 200
func (h *Handlers) Create(...) (*Created[DestinationDisplay], CreateErrors) // 201
func (h *Handlers) Delete(...) (*NoContent, DeleteErrors) // 204
Pagination
List endpoints return paginated responses matching the Hookdeck API paging format:
type Pagination struct {
OrderBy string `json:"order_by"`
Dir string `json:"dir"` // "asc" | "desc"
Limit int `json:"limit"`
Next string `json:"next,omitempty"`
Prev string `json:"prev,omitempty"`
}
type Paginated[T any] struct {
Models []T `json:"models"`
Pagination Pagination `json:"pagination"`
}
Usage:
func (h *Handlers) List(...) (*Paginated[DestinationDisplay], ListErrors)
Response:
{
"models": [{"id": "dest_123", ...}, ...],
"pagination": {
"order_by": "created_at",
"dir": "desc",
"limit": 100,
"next": "dest_456",
"prev": null
}
}
Input Types
Struct tags define where values come from:
type CreateDestinationInput struct {
TenantScopedInput // *Tenant from middleware
Body CreateDestinationRequest `json:",inline"` // Request body
}
type ListEventsInput struct {
TenantScopedInput
Topics []string `form:"topic"` // Query param (repeated)
Limit int `form:"limit" binding:"max=100"`
Cursor string `form:"cursor"`
}
Reflection extracts these to generate OpenAPI parameters.
Error Types
Error responses match the Hookdeck API error format:
// Base error struct - all errors embed this
type APIError struct {
Level string `json:"level"` // "info" | "error"
Handled bool `json:"handled"` // true if expected error
Report bool `json:"report"` // true if should be reported
Status int `json:"status"` // HTTP status code
Code string `json:"code"` // "NOT_FOUND", "CONFLICT", etc.
}
func (e APIError) HTTPStatus() int { return e.Status }
// Specific errors embed APIError and add extra fields as needed
type NotFoundError struct {
APIError
}
type ConflictError struct {
APIError
}
type ValidationError struct {
APIError
Data []string `json:"data"` // ["name is required", "email is invalid"]
}
Example response:
{
"level": "info",
"handled": true,
"report": true,
"data": ["name is required"],
"status": 422,
"code": "UNPROCESSABLE_ENTITY"
}
ErrorSet - Declaring Possible Errors
Each operation declares which errors it can return:
type CreateDestinationErrors struct {
BaseErrors
Conflict *ConflictError // 409 - ID already exists
}
type GetDeliveryErrors struct {
BaseErrors
NotFound *NotFoundError // 404 - delivery not found
}
The struct fields are pointer types. Returning a non-nil pointer means that error occurred. Reflection walks the struct to discover possible errors for OpenAPI.
Implicit vs Explicit Errors
Implicit (added to all operations automatically):
- 400 BadRequestError - malformed request body
- 401 UnauthorizedError - auth failure
- 422 ValidationError - input validation failed
- 500 InternalServerError - unexpected error
Explicit (declared per operation via ErrorSet):
- 404, 409, etc.
Handlers only declare operation-specific errors. Common errors are handled by framework.
Handler Implementation
Handlers are pure functions - no Gin dependency, testable:
func (h *DestinationHandlers) Create(
ctx context.Context,
input CreateDestinationInput,
) (*Created[DestinationDisplay], CreateDestinationErrors) {
existing, _ := h.store.GetDestination(ctx, input.Tenant.ID, input.Body.ID)
if existing != nil {
return nil, CreateDestinationErrors{
Conflict: &ConflictError{Message: "destination already exists"},
}
}
dest, _ := h.store.CreateDestination(ctx, input.Tenant.ID, input.Body)
return &Created[DestinationDisplay]{dest}, CreateDestinationErrors{}
}
Registration
var CreateDestination = Operation[CreateDestinationInput, Created[DestinationDisplay], CreateDestinationErrors]{
Method: "POST",
Path: "/tenants/:tenantID/destinations",
Auth: AuthTenant,
TenantScoped: true,
OpenAPI: OpenAPIMeta{
Summary: "Create destination",
Tags: []string{"Destinations"},
OperationID: "createDestination",
},
}
// In router setup - Operation implements Route interface
CreateDestination.Handler = handlers.Destination.Create
routes := []Route{CreateDestination, GetDestination, ListDestinations, ...}
OpenAPI Generation
At startup, for each operation:
- Reflect on
Intype → extract path/query/body parameters - Reflect on
Outtype:- Check if it implements
HTTPStatus()→ use that status code, else 200 - Unwrap generic wrappers (
Created[T]→T) for the response schema
- Check if it implements
- Reflect on
Errtype → extract error response schemas from struct fields - Add implicit errors (400, 401, 422, 500)
- Build OpenAPI operation and add to spec
Served at GET /api/v1/openapi.json.
Alternatives Considered
Schema-First (oapi-codegen, ogen)
Write OpenAPI YAML first, then generate Go types and handlers from it.
Rejected because: Code-first development is the preferred workflow. Writing Go is more natural than context-switching to YAML, and avoids maintaining separate tooling for schema generation. The API should emerge from the code, not the other way around.
Open Questions
- Polymorphic destinations - Destinations have type-specific config/credentials. How do we generate
oneOfschemas? Likely leveragedestregistry.ProviderMetadata.
Dependencies
- Auth Refactor (prerequisite) - Provides simplified
AuthMode(Public, Tenant, Admin) andTenantScopedcontract with*Tenantinjection - kin-openapi library - For building OpenAPI 3.x spec programmatically
Build-Time Generation (Optional)
If needed for SDK generation or CI workflows, add CLI that dumps runtime spec to file:
go run ./cmd/openapi-gen -output=docs/apis/openapi.yaml
- Lingua principale
- Go
- Stelle
- 1.1k
- Fork
- 70
- Merge medio
- 1g 3h
- PR unite (30g)
- 50
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di hookdeck/outpost
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
I maintainer di solito rispondono entro 1 giorno
-
enhancement
Difficoltà 4/5 3-5 giorni Idoneità per principianti 48/100
hookdeck/outpost#1117 · 1 commento · 1 reazione ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 35/100
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di hookdeck/outpost
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
-
agent-review-finding bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
jordansmall/spindrift#5281 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
github/github-mcp-server#3489 ·
I maintainer di solito rispondono entro 4 giorni
-
L: java:gradle L: java:maven L: ruby:bundler T: bug 🐞
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
dependabot/dependabot-core#16547 ·
I maintainer di solito rispondono entro 1 giorno
-
Docs/examples still create key ranges in ascending order, which fails in 3.1.0 (follow-up to #1754)Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
pg-sharding/spqr#3083 ·
I maintainer di solito rispondono entro 1 giorno