-
Notifications
You must be signed in to change notification settings - Fork 0
OpenAPI Specification
Neoma serves the OpenAPI spec and related resources at configurable paths:
| Endpoint | Content-Type | Description |
|---|---|---|
GET /openapi.json |
application/openapi+json |
OpenAPI spec in JSON |
GET /openapi.yaml |
application/openapi+yaml |
OpenAPI spec in YAML |
GET /schemas/{name} |
application/json |
Individual JSON Schema by name |
GET /public/docs |
text/html |
Interactive docs UI |
Paths are configurable via Config.OpenAPIPath, Config.SchemasPath, and Config.Docs.Path.
Schema $ref values in the individual schema endpoint are rewritten to point to sibling schema URLs (e.g., #/components/schemas/Foo becomes /schemas/Foo.json).
The internal representation always uses OpenAPI 3.2. The version served to clients is controlled by Config.OpenAPIVersion:
| Config Value | Constant | Behavior |
|---|---|---|
"3.2.0" |
core.OpenAPIVersion32 |
Served as-is. This is the default. |
"3.1.0" |
core.OpenAPIVersion31 |
Served as-is (3.1 and 3.2 are structurally identical). |
"3.0.3" |
core.OpenAPIVersion30 |
Downgraded at serve time via openapi.Downgrade. |
The 3.0 downgrade is a post-marshal transformation that converts:
- Type arrays (
["string", "null"]) totype+nullable: true - Numeric
exclusiveMinimum/exclusiveMaximumto boolean form -
examplesarray to singularexample -
contentEncodingtox-contentEncoding - Binary upload schemas
This downgrade is lossy (e.g., multiple examples collapse to one). 3.1 and 3.2 require no transformation.
OpenAPI 3.2 supports nested tags. The core.Tag struct includes a Tags []*Tag field for defining tag hierarchies:
config.OpenAPI.Tags = []*core.Tag{
{
Name: "Items",
Description: "Item management",
Tags: []*core.Tag{
{Name: "Items.Admin", Description: "Admin-only item operations"},
},
},
}Nested tags appear in the spec under the parent tag's tags array, which doc UIs that support OAS 3.2 can render as a hierarchy.
Schemas are generated automatically from input and output struct types. The schema package provides two entry points:
-
schema.FromField(registry, structField, hint): Generates a schema for a struct field, applying all struct tag constraints. -
schema.FromType(registry, reflectType): Generates a schema for a Go type.
The schema registry handles deduplication and $ref resolution.
type Item struct {
ID int `json:"id" doc:"Unique identifier"`
Name string `json:"name" minLength:"1" maxLength:"255" doc:"Item name"`
CreatedAt time.Time `json:"created_at" doc:"Creation timestamp"`
}Generates:
{
"type": "object",
"properties": {
"id": { "type": "integer", "description": "Unique identifier" },
"name": { "type": "string", "minLength": 1, "maxLength": 255, "description": "Item name" },
"created_at": { "type": "string", "format": "date-time", "description": "Creation timestamp" }
}
}Types can supply their own schema by implementing core.SchemaProvider:
func (m MyType) Schema(r core.Registry) *core.Schema {
return &core.Schema{
Type: core.TypeString,
Format: "my-format",
}
}Types can modify their generated schema after generation by implementing core.SchemaTransformer:
func (m MyType) TransformSchema(r core.Registry, s *core.Schema) *core.Schema {
s.Description = "Custom description"
return s
}Override the default schema naming:
config.SchemaNamer = func(t reflect.Type, hint string) string {
return t.Name()
}| Struct Tag | OpenAPI Field |
|---|---|
doc:"..." |
description |
example:"..." |
examples |
deprecated:"true" |
deprecated |
readOnly:"true" |
readOnly |
writeOnly:"true" |
writeOnly |
Operation-level metadata is set on the core.Operation struct:
neoma.Register(api, core.Operation{
Tags: []string{"Items"},
Summary: "List items",
Description: "Returns all items with pagination.",
Deprecated: true,
}, handler)Request body examples are auto-built from example tags on input Body struct fields, the same way response examples work:
type CreateItemInput struct {
Body struct {
Name string `json:"name" example:"Widget"`
Price float64 `json:"price" example:"9.99"`
}
}The generated OpenAPI spec includes an example object on the request body media type built from these tag values.
Success response examples come from example tags on output struct fields:
type GetItemOutput struct {
Body struct {
ID int `json:"id" example:"42"`
Name string `json:"name" example:"Widget"`
}
}Errors listed in op.Errors are added to the OpenAPI spec with the schema from ErrorHandler.ErrorSchema and the content type from ErrorHandler.ErrorContentType.
The neoma-discover tool generates per-handler error metadata at build time. At registration, these are matched to the handler and used to produce examples in the spec:
{
"responses": {
"404": {
"description": "Not Found",
"content": {
"application/problem+json": {
"schema": { "$ref": "#/components/schemas/ProblemDetail" },
"example": {
"type": "/errors/404",
"title": "Not Found",
"status": 404,
"detail": "item not found"
}
}
}
}
}
}When multiple errors share the same HTTP status code, they appear as separate named entries in the examples map.
ErrorHeaders adds headers to every error response for an operation. ErrorResponses provides per-status customization (description, headers, schema override). Per-status headers take priority over global error headers.
op := core.Operation{
Errors: []int{400, 409},
ErrorHeaders: map[string]*core.Param{
"X-Request-ID": {
Description: "Unique request identifier",
Schema: &core.Schema{Type: core.TypeString},
},
},
ErrorResponses: map[int]*core.ErrorResponseConfig{
409: {
Description: "Resource conflict",
},
},
}Protect spec and schema endpoints with middleware via Config.SpecMiddlewares:
config.SpecMiddlewares = core.Middlewares{func(ctx core.Context, next func(core.Context)) {
if ctx.Header("Authorization") == "" {
ctx.SetStatus(401)
return
}
next(ctx)
}}The docs UI endpoint has its own middleware via Config.Docs.Middlewares.
The openapi package provides functions to filter the spec by tag:
import "github.com/MeGaNeKoS/neoma/openapi"
// Only operations tagged "public"
publicSpec := openapi.FilterByTag(api.OpenAPI(), "public")
// Everything except "internal" tagged operations
filteredSpec := openapi.FilterExcludeTag(api.OpenAPI(), "internal")Programmatic access to the spec in both formats:
import "github.com/MeGaNeKoS/neoma/openapi"
// Native version (3.1/3.2)
jsonBytes, err := json.Marshal(api.OpenAPI())
yamlBytes, err := openapi.YAML(api.OpenAPI())
// Downgraded to 3.0
json30, err := openapi.Downgrade(api.OpenAPI())
yaml30, err := openapi.DowngradeYAML(api.OpenAPI())React to operations being added to the spec:
config.OpenAPI.OnAddOperation = []func(oapi *core.OpenAPI, op *core.Operation){
func(oapi *core.OpenAPI, op *core.Operation) {
if op.Security == nil {
op.Security = []map[string][]string{
{"bearer": {}},
}
}
},
}