zoobzio December 15, 2025 Edit this page

OpenAPI Generation Guide

Rocco automatically generates OpenAPI 3.1.0 specifications from your handlers. This guide covers customization, schema tags, and advanced patterns.

Default Endpoints

When you register any handler, rocco automatically sets up:

  • /openapi - OpenAPI JSON specification
  • /docs - Interactive Scalar documentation
engine := rocco.NewEngine()
engine.WithHandlers(handler)
engine.Start(rocco.HostAll, 8080)

// Visit http://localhost:8080/docs for interactive docs

Customizing API Info

engine.WithOpenAPIInfo(openapi.Info{
    Title:          "My API",
    Description:    "API for managing resources",
    Version:        "1.0.0",
    TermsOfService: "https://example.com/terms",
    Contact: &openapi.Contact{
        Name:  "API Support",
        URL:   "https://example.com/support",
        Email: "support@example.com",
    },
    License: &openapi.License{
        Name: "MIT",
        URL:  "https://opensource.org/licenses/MIT",
    },
})

Tags

Tags group operations in the documentation:

Handler Tags

handler := rocco.NewHandler[Input, Output](
    "create-user",
    "POST",
    "/users",
    createUser,
).WithTags("users", "admin")

Engine Tags (with descriptions)

engine.WithTag("users", "User management operations")
engine.WithTag("orders", "Order processing and tracking")
engine.WithTag("admin", "Administrative operations")

Tag Groups

Tag groups organize tags into hierarchical categories via the x-tagGroups vendor extension. Documentation tools like Redoc use these to create sidebar sections:

engine.
    WithTagGroup("Account", "users", "auth").
    WithTagGroup("Commerce", "orders", "payments").
    WithTagGroup("Admin", "admin", "audit")

Handler Documentation

handler := rocco.NewHandler[CreateUserInput, UserOutput](
    "create-user",
    "POST",
    "/users",
    createUser,
).
    WithSummary("Create a new user").
    WithDescription(`
Creates a new user account with the provided information.

The email must be unique across all users. If a user with the same
email already exists, a 409 Conflict error is returned.

**Required permissions:** users:write
`).
    WithTags("users")

Schema Generation

Rocco generates JSON schemas from your Go types using sentinel.

Basic Type Mapping

Go TypeJSON Schema Type
stringstring
int, int32, int64integer
float32, float64number
boolboolean
[]Tarray
structobject
*Tnullable T
map[string]Tobject with additionalProperties

Struct Tags

JSON Tag

type User struct {
    ID        string `json:"id"`           // Field name in JSON
    FirstName string `json:"first_name"`   // Snake case
    Email     string `json:"email"`
    Internal  string `json:"-"`            // Excluded from schema
}

Description Tag

type CreateUserInput struct {
    Name  string `json:"name" description:"User's full name"`
    Email string `json:"email" description:"Primary email address"`
    Age   int    `json:"age" description:"Age in years"`
}

Example Tag

type CreateUserInput struct {
    Name  string `json:"name" example:"John Doe"`
    Email string `json:"email" example:"john@example.com"`
    Age   int    `json:"age" example:"25"`
}

Examples are type-aware:

  • String: example:"hello""hello"
  • Integer: example:"42"42
  • Float: example:"3.14"3.14
  • Boolean: example:"true"true
  • Array: example:"a,b,c"["a", "b", "c"]

Discriminator Tag

type Notification struct {
    Type  string `json:"type" discriminator:"event"` // "I select for the event field"
    Event any    `json:"event" discriminate:"TypeA,TypeB"` // "I am the union"
}

See Discriminated Unions for full details.

Validation to OpenAPI Mapping

The validate tag drives both runtime validation and OpenAPI constraints:

ValidatorApplies ToOpenAPI Mapping
requiredallrequired array
min=Nnumbersminimum
max=Nnumbersmaximum
min=NstringsminLength
max=NstringsmaxLength
gte=Nnumbersminimum
lte=Nnumbersmaximum
gt=Nnumbersminimum + exclusiveMinimum
lt=Nnumbersmaximum + exclusiveMaximum
len=NarraysminItems + maxItems
len=NstringsminLength + maxLength
uniquearraysuniqueItems
emailstringsformat: "email"
urlstringsformat: "uri"
uuidstringsformat: "uuid"
datetimestringsformat: "date-time"
ipv4stringsformat: "ipv4"
ipv6stringsformat: "ipv6"
oneof=a b canyenum: ["a", "b", "c"]

Example

type CreateUserInput struct {
    Name     string   `json:"name" validate:"required,min=2,max=100" description:"Full name"`
    Email    string   `json:"email" validate:"required,email" description:"Email address"`
    Age      int      `json:"age" validate:"gte=0,lte=150" description:"Age in years"`
    Role     string   `json:"role" validate:"oneof=admin user guest" description:"User role"`
    Tags     []string `json:"tags" validate:"max=10,unique" description:"User tags"`
}

Generated schema:

CreateUserInput:
  type: object
  required: [name, email]
  properties:
    name:
      type: string
      minLength: 2
      maxLength: 100
      description: Full name
    email:
      type: string
      format: email
      description: Email address
    age:
      type: integer
      minimum: 0
      maximum: 150
      description: Age in years
    role:
      type: string
      enum: [admin, user, guest]
      description: User role
    tags:
      type: array
      maxItems: 10
      uniqueItems: true
      description: User tags

Standalone Models

Types that aren't directly used as handler input or output can be registered as standalone models. This is required for discriminated unions and useful for any type you want in your OpenAPI component schemas.

engine.WithModels(
    rocco.NewModel[IngestCompletedEvent](),
    rocco.NewModel[IngestFailedEvent](),
)

Discriminated Unions

For polymorphic payloads where a type field determines the shape of a nested object, use the discriminator and discriminate struct tags together.

Tags

  • discriminator:"fieldName" on the selector field — declares "I select for this union field" and names the json property name of the target union field (not the Go field name)
  • discriminate:"TypeA,TypeB" on the union field — declares "I am the union" and lists the possible schemas
type Notification struct {
    Type  string `json:"type" discriminator:"event"`
    Event any    `json:"event" discriminate:"IngestCompletedEvent,IngestFailedEvent"`
}

type IngestCompletedEvent struct {
    DocumentID   string `json:"document_id"`
    DocumentName string `json:"document_name"`
}

type IngestFailedEvent struct {
    DocumentID string `json:"document_id"`
    Error      string `json:"error"`
}

Register the variant types as standalone models to ensure they appear in the spec (this is only strictly necessary when the types aren't already scanned through handler input/output types):

engine := rocco.NewEngine().
    WithModels(
        rocco.NewModel[IngestCompletedEvent](),
        rocco.NewModel[IngestFailedEvent](),
    ).
    WithHandlers(notificationHandler)

Generated Schema

Notification:
  type: object
  properties:
    type:
      type: string
    event:
      oneOf:
        - $ref: '#/components/schemas/IngestCompletedEvent'
        - $ref: '#/components/schemas/IngestFailedEvent'
      discriminator:
        propertyName: type
        mapping:
          IngestCompletedEvent: '#/components/schemas/IngestCompletedEvent'
          IngestFailedEvent: '#/components/schemas/IngestFailedEvent'

Error Schemas

Declared errors generate response schemas:

handler.WithErrors(rocco.ErrNotFound, rocco.ErrConflict)

Generates:

responses:
  404:
    description: Not Found
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/ErrNotFound'
  409:
    description: Conflict
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/ErrConflict'

Custom Error Schemas

type InsufficientFundsDetails struct {
    Required  float64 `json:"required" description:"Amount required"`
    Available float64 `json:"available" description:"Amount available"`
}

var ErrInsufficientFunds = rocco.NewError[InsufficientFundsDetails](
    "INSUFFICIENT_FUNDS", 402, "insufficient funds",
)

handler.WithErrors(ErrInsufficientFunds)

The details fields are inlined directly on the error schema as the details property.

Parameters

Path Parameters

handler := rocco.NewHandler[rocco.NoBody, User](
    "get-user",
    "GET",
    "/users/{id}",
    getUser,
).WithPathParams("id")

Generates:

parameters:
  - name: id
    in: path
    required: true
    schema:
      type: string

Query Parameters

handler := rocco.NewHandler[rocco.NoBody, UserList](
    "list-users",
    "GET",
    "/users",
    listUsers,
).WithQueryParams("page", "limit", "sort")

Generates:

parameters:
  - name: page
    in: query
    schema:
      type: string
  - name: limit
    in: query
    schema:
      type: string
  - name: sort
    in: query
    schema:
      type: string

Security Schemes

For authenticated handlers:

handler.WithAuthentication()
handler.WithScopes("users:read")

The OpenAPI spec includes security requirements.

Programmatic Access

Generate the spec programmatically:

spec := engine.GenerateOpenAPI(nil)

// Serialize to JSON
data, _ := json.MarshalIndent(spec, "", "  ")

// Write to file
os.WriteFile("openapi.json", data, 0644)

Best Practices

1. Use Descriptive Names

// GOOD - clear handler names
NewHandler[Input, Output]("create-user", ...)
NewHandler[Input, Output]("list-user-orders", ...)

// AVOID - vague names
NewHandler[Input, Output]("handler1", ...)

2. Document All Fields

type CreateUserInput struct {
    Name  string `json:"name" description:"User's full legal name" example:"John Doe"`
    Email string `json:"email" description:"Primary email for notifications" example:"john@example.com"`
}

3. Use Consistent Tags

// Group by resource
handler.WithTags("users")
handler.WithTags("orders")

// Add descriptions
engine.WithTag("users", "User management operations")

4. Validate Everything

type Input struct {
    Email string `json:"email" validate:"required,email"`  // Both validated AND documented
}

5. Declare All Errors

handler.WithErrors(
    rocco.ErrNotFound,
    rocco.ErrConflict,
    rocco.ErrForbidden,
) // All appear in OpenAPI spec

See Also