Skip to content

OpenAPI Specification

MeGaNeKo edited this page Mar 20, 2026 · 1 revision

OpenAPI Specification

Served Endpoints

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).

OpenAPI Version Support

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"]) to type + nullable: true
  • Numeric exclusiveMinimum/exclusiveMaximum to boolean form
  • examples array to singular example
  • contentEncoding to x-contentEncoding
  • Binary upload schemas

This downgrade is lossy (e.g., multiple examples collapse to one). 3.1 and 3.2 require no transformation.

OAS 3.2 Tag Nesting

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.

Schema Generation from Go Types

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" }
    }
}

SchemaProvider Interface

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",
    }
}

SchemaTransformer Interface

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
}

Custom Schema Namer

Override the default schema naming:

config.SchemaNamer = func(t reflect.Type, hint string) string {
    return t.Name()
}

Response Documentation

Struct Tag to OpenAPI Mapping

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

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 Examples

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"`
    }
}

Error Documentation

Per-Endpoint Error Responses

Errors listed in op.Errors are added to the OpenAPI spec with the schema from ErrorHandler.ErrorSchema and the content type from ErrorHandler.ErrorContentType.

Auto-Discovered Examples

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 and ErrorResponses

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",
        },
    },
}

Spec Middleware

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.

Spec Filtering

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")

JSON and YAML Output

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())

OnAddOperation Hook

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": {}},
            }
        }
    },
}

Clone this wiki locally