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 Type | JSON Schema Type |
|---|---|
string | string |
int, int32, int64 | integer |
float32, float64 | number |
bool | boolean |
[]T | array |
struct | object |
*T | nullable T |
map[string]T | object 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:
| Validator | Applies To | OpenAPI Mapping |
|---|---|---|
required | all | required array |
min=N | numbers | minimum |
max=N | numbers | maximum |
min=N | strings | minLength |
max=N | strings | maxLength |
gte=N | numbers | minimum |
lte=N | numbers | maximum |
gt=N | numbers | minimum + exclusiveMinimum |
lt=N | numbers | maximum + exclusiveMaximum |
len=N | arrays | minItems + maxItems |
len=N | strings | minLength + maxLength |
unique | arrays | uniqueItems |
email | strings | format: "email" |
url | strings | format: "uri" |
uuid | strings | format: "uuid" |
datetime | strings | format: "date-time" |
ipv4 | strings | format: "ipv4" |
ipv6 | strings | format: "ipv6" |
oneof=a b c | any | enum: ["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
- Handler Guide - Handler configuration
- Error Handling - Error patterns
- API Reference - Complete API documentation