Skip to content

OpenAPI Spec Generation

See also: render/openapi on pkg.go.dev · api/rest on pkg.go.dev

Runnable demos: examples/openapi · examples/rest-api · examples/api-rest

go-codex generates OpenAPI 3.1 documents from the same codec definitions that drive your runtime encode/decode — no separate YAML authoring, no drift.

Components/schemas only

Use render/openapi to render codec schemas directly:

import (
    "github.com/DaniDeer/go-codex/render/openapi"
    "github.com/DaniDeer/go-codex/validate"
)

var UserCodec = codex.Struct[User](
    codex.RequiredField("name",
        codex.String().
            Refine(validate.NonEmptyString).
            Refine(validate.MaxLen(100)).
            WithTitle("Full Name").
            WithDescription("The user's full display name."),
        func(u User) string { return u.Name },
        func(u *User, v string) { u.Name = v },
    ),
    codex.RequiredField("age",
        codex.Int().Refine(validate.RangeInt(0, 150)).WithDescription("Age in years."),
        func(u User) int { return u.Age },
        func(u *User, v int) { u.Age = v },
    ),
)

// Render components/schemas as YAML — ready to paste into openapi.yaml.
yamlBytes, err := openapi.MarshalYAML(map[string]schema.Schema{
    "User": UserCodec.Schema,
})

Output:

User:
  type: object
  properties:
    name:
      type: string
      title: Full Name
      description: The user's full display name.
      minLength: 1
      maxLength: 100
    age:
      type: integer
      description: Age in years.
      minimum: 0
      maximum: 150
  required: [name, age]

Constraint schema reflection is automatic: validate.* constraints (MinLen, RangeInt, OneOf, Pattern, Email, UUID, etc.) annotate the schema. Custom constraints can do the same by setting Constraint.Schema.

Full OpenAPI 3.1 document via api/rest

The api/rest builder generates a complete OpenAPI 3.1 document from all registered routes. The same builder that drives runtime decode/encode/validate also produces the spec:

b := rest.NewBuilder(
    rest.Info{Title: "User API", Version: "1.0.0"},
    rest.WithPathConstraints(validate.HTTPPath),
)
b.AddServer("production", rest.Server{URL: "https://api.example.com/v1"})

// Register routes — path, request/response codecs, and parameter codecs all
// flow into the spec automatically.
createUser, _ := rest.NewRoute[CreateUserReq, User]("POST", "/users",
    createUserCodec, userCodec,
    rest.RouteMeta{
        OperationID:    "createUser",
        Summary:        "Create a user",
        ReqSchemaName:  "CreateUserRequest",  // → $ref in spec
        RespSchemaName: "User",
        RespStatus:     "201",
    },
).Register(b)

uuidCodec := codex.String().Refine(validate.UUID)
getUser, _ := rest.NewRoute[struct{}, User]("GET", "/users/{id}",
    codex.Empty, userCodec,
    rest.RouteMeta{OperationID: "getUser", RespSchemaName: "User"},
    // PathParam.Codec validates {id} at BuildPath time + UUID schema → spec
    rest.PathParam{Name: "id", Description: "User UUID"}.WithCodec(uuidCodec),
).Register(b)

// Query params flow into spec as in: query parameters
pageCodec := codex.String().Refine(validate.NonNegativeIntString)
listUsers, _ := rest.NewRoute[struct{}, []User]("GET", "/users",
    codex.Empty, codex.SliceOf(userCodec),
    rest.RouteMeta{OperationID: "listUsers"},
    rest.QueryParam{Name: "page", Description: "Page number (0-based)"}.WithCodec(pageCodec),
    rest.QueryParam{Name: "search", Description: "Name filter"},
).Register(b)

// Cookie + header params flow into spec as in: cookie / in: header parameters
sessionCodec  := codex.String().Refine(validate.NonEmptyString)
requestIDCodec := codex.String().Refine(validate.UUID)
profile, _ := rest.NewRoute[struct{}, User]("GET", "/profile",
    codex.Empty, userCodec,
    rest.RouteMeta{OperationID: "getProfile"},
    rest.CookieParam{Name: "session_token", Required: true}.WithCodec(sessionCodec),
    rest.HeaderParam{Name: "X-Request-Id",  Required: true}.WithCodec(requestIDCodec),
).Register(b)

// Generate the full OpenAPI 3.1 spec from all registered routes:
doc, err := b.OpenAPISpec()
yamlBytes, _ := doc.MarshalYAML()

Security schemes

b.AddSecurityScheme("bearerAuth", rest.SecurityScheme{
    SecurityScheme: route.BearerScheme("JWT"),
}.WithCodec(codex.String().Refine(validate.BearerToken)))

b.AddGlobalSecurity(route.Require("bearerAuth"))

// Per-route override — empty slice = no auth required for this route
createUser, _ := rest.NewRoute[CreateUserReq, User]("POST", "/users", ...,
    rest.RouteMeta{
        Security: []route.SecurityRequirement{
            route.Require("bearerAuth", "write:users"),
        },
    },
).Register(b)

Security schemes appear in components/securitySchemes; global security at document root; per-operation security overrides inline — all generated automatically.

Using the lower-level DocumentBuilder directly

For cases where you build route descriptors manually (without api/rest):

import (
    "github.com/DaniDeer/go-codex/render/openapi"
    "github.com/DaniDeer/go-codex/route"
)

doc, err := openapi.NewDocumentBuilder(openapi.Info{
    Title:   "User API",
    Version: "1.0.0",
}).
    AddServer(openapi.Server{URL: "https://api.example.com/v1"}).
    AddRoute(route.Route{
        Method:      "POST",
        Path:        "/users",
        OperationID: "createUser",
        RequestBody: &route.Body{
            Required:   true,
            Schema:     CreateUserCodec.Schema,
            SchemaName: "CreateUserRequest",  // → $ref + registered in components
        },
        Responses: []route.Response{
            {Status: "201", Schema: &UserCodec.Schema, SchemaName: "User"},
            {Status: "400", Description: "Validation error."},
        },
    }).
    Build()

Build() validates: - No duplicate (method, path) pairs. - PathParams names exactly match {placeholder} segments in the path. - Path parameters are always required: true in the output.

See also