zoobzio January 15, 2026 3 mins Edit this page

OpenAPI Generation (rocco)

What is rocco?

rocco is a type-safe HTTP framework. Handlers are generic—Handler[In, Out]—so request and response types are known at compile time. From these types, rocco automatically generates OpenAPI 3.1.0 specifications.

The spec is served at /openapi, with interactive documentation at /docs.

The Problem

API documentation drifts. You update a handler, forget to update the spec, and clients break. Manual spec maintenance doesn't scale.

How Sentinel Enables This

When you create a handler, rocco calls sentinel.Scan on both input and output types:

handler := rocco.NewHandler[CreateUserRequest, User](
    "create-user", "POST", "/users",
    func(req *rocco.Request[CreateUserRequest]) (User, error) {
        // ...
    },
)

Internally, rocco extracts metadata for schema generation:

// rocco calls this at handler creation:
sentinel.Scan[CreateUserRequest]()
sentinel.Scan[User]()

Scan (not Inspect) is key—it recursively discovers all related types within your module. If User has a *Profile field, sentinel finds Profile too.

Define types with validation and documentation tags:

type CreateUserRequest struct {
    Name  string `json:"name" validate:"required,min=3,max=100" description:"User's full name"`
    Email string `json:"email" validate:"required,email" description:"Contact email"`
    Age   int    `json:"age,omitempty" validate:"gte=0,lte=150"`
}

Sentinel extracts the metadata:

// metadata.Fields contains:
// - {Name: "Name", Tags: {"json": "name", "validate": "required,min=3,max=100", "description": "User's full name"}}
// - {Name: "Email", Tags: {"json": "email", "validate": "required,email", "description": "Contact email"}}
// - {Name: "Age", Tags: {"json": "age,omitempty", "validate": "gte=0,lte=150"}}

rocco transforms this to OpenAPI schemas:

{
  "type": "object",
  "required": ["name", "email"],
  "properties": {
    "name": {
      "type": "string",
      "minLength": 3,
      "maxLength": 100,
      "description": "User's full name"
    },
    "email": {
      "type": "string",
      "format": "email",
      "description": "Contact email"
    },
    "age": {
      "type": "integer",
      "minimum": 0,
      "maximum": 150
    }
  }
}

Change the struct, the docs update.

What Sentinel Provides

rocco needsSentinel provides
Property namesFieldMetadata.Tags["json"]
Property typesFieldMetadata.Type → OpenAPI type mapping
Required fieldsTags["validate"] containing required
ConstraintsTags["validate"] → OpenAPI constraints
DescriptionsTags["description"]
ExamplesTags["example"]
Nested schemasMetadata.Relationships for $ref generation

Sentinel's recursive scanning means rocco discovers your entire API surface from handler types alone.

Tag Mappings

Validate TagOpenAPI
requiredfield in required array
min=3minLength (strings) or minimum (numbers)
max=100maxLength or maximum
gt=0exclusiveMinimum
lt=100exclusiveMaximum
emailformat: "email"
urlformat: "uri"
uuidformat: "uuid"
datetimeformat: "date-time"
oneof=a b cenum: ["a", "b", "c"]
uniqueuniqueItems: true (arrays)

Nested Types

Sentinel's relationship discovery handles complex types:

type Order struct {
    ID       string    `json:"id"`
    Customer *Customer `json:"customer"`
    Items    []Item    `json:"items"`
}

When Order is scanned, sentinel discovers Customer and Item via Metadata.Relationships. rocco uses sentinel.Lookup() to retrieve each type's metadata and includes all three in components.schemas with proper $ref links.

Learn More