Documenting generic responses
A common Go pattern is a single response envelope — a JSend-style wrapper that
every handler returns, with an open any (interface{}) field for the payload:
// APIResponse is the one generic envelope every handler returns. Because Data is
// an open type (any, i.e. interface{}), the scanner can only render it as an
// open schema — it has no way to know which concrete payload a given operation
// puts there.
//
// swagger:model
type APIResponse struct {
Status string `json:"status"`
Data any `json:"data"`
Message string `json:"message,omitempty"`
}Full source: docs/examples/shaping/genericenvelopes/genericenvelopes.go
{
"description": "APIResponse is the one generic envelope every handler returns. Because Data is\nan open type (any, i.e. interface{}), the scanner can only render it as an\nopen schema — it has no way to know which concrete payload a given operation\nputs there.",
"type": "object",
"properties": {
"data": {
"x-go-name": "Data"
},
"message": {
"type": "string",
"x-go-name": "Message"
},
"status": {
"type": "string",
"x-go-name": "Status"
}
},
"x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/genericenvelopes"
}
Full source: docs/examples/shaping/genericenvelopes/testdata/apiresponse.json
codescan reads this faithfully: Data is any, so in the spec it becomes an
open schema — {"x-go-name":"Data"}, no type, no $ref. That is correct
(the field genuinely accepts anything), but it is not what you want in an API
contract, where each operation returns a specific payload.
codescan will not grow a per-route override syntax like swaggo’s
body:APIResponse{Data: StatusReport} — that asks the scanner to invent a type
the code never declares. Instead, declare the type: a doc-only struct that
mirrors the envelope but pins the payload.
Embed the envelope, shadow the payload
The DRY way is to embed the generic envelope — promoting its Status and
Message — and re-declare only the one opaque field with a concrete type:
// StatusEnvelope documents the /status response: it embeds APIResponse and
// shadows the open Data with the concrete StatusReport. Handlers keep returning
// APIResponse — this type just gives the scanner a concrete shape.
//
// swagger:model
type StatusEnvelope struct {
APIResponse
// Data carries the concrete status report.
Data StatusReport `json:"data"`
}Full source: docs/examples/shaping/genericenvelopes/genericenvelopes.go
{
"description": "StatusEnvelope documents the /status response: it embeds APIResponse and\nshadows the open Data with the concrete StatusReport. Handlers keep returning\nAPIResponse — this type just gives the scanner a concrete shape.",
"type": "object",
"properties": {
"data": {
"x-go-name": "Data",
"$ref": "#/definitions/StatusReport"
},
"message": {
"type": "string",
"x-go-name": "Message"
},
"status": {
"type": "string",
"x-go-name": "Status"
}
},
"x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/genericenvelopes"
}
Full source: docs/examples/shaping/genericenvelopes/testdata/statusenvelope.json
The locally-declared Data is shallower than the embedded one, so it wins —
both in codescan’s view and in encoding/json at runtime. The result is a clean
flat object whose data is a concrete $ref, with status and message
carried over from the embed:
{
"description": "APIResponse is the one generic envelope every handler returns. Because Data is\nan open type (any, i.e. interface{}), the scanner can only render it as an\nopen schema — it has no way to know which concrete payload a given operation\nputs there.",
"type": "object",
"properties": {
"data": {
"x-go-name": "Data"
},
"message": {
"type": "string",
"x-go-name": "Message"
},
"status": {
"type": "string",
"x-go-name": "Status"
}
},
"x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/genericenvelopes"
}
Full source: docs/examples/shaping/genericenvelopes/testdata/apiresponse.json
{
"description": "StatusEnvelope documents the /status response: it embeds APIResponse and\nshadows the open Data with the concrete StatusReport. Handlers keep returning\nAPIResponse — this type just gives the scanner a concrete shape.",
"type": "object",
"properties": {
"data": {
"x-go-name": "Data",
"$ref": "#/definitions/StatusReport"
},
"message": {
"type": "string",
"x-go-name": "Message"
},
"status": {
"type": "string",
"x-go-name": "Status"
}
},
"x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/genericenvelopes"
}
Full source: docs/examples/shaping/genericenvelopes/testdata/statusenvelope.json
You restate one field, not the whole envelope. Point the route’s response at
the doc-only struct with the body: sub-language,
and specialise the same envelope per operation with a different payload each
time:
// swagger:route GET /status status getStatus
//
// Returns the service status wrapped in the generic envelope. The response is
// declared as the doc-only StatusEnvelope, so data is the concrete StatusReport
// rather than an open schema.
//
// Responses:
// 200: body:StatusEnvelope the status envelope
// swagger:route GET /users/{id} users getUser
//
// Returns a user wrapped in the same envelope, specialised to UserSummary.
//
// Responses:
// 200: body:UserEnvelope the user envelopeFull source: docs/examples/shaping/genericenvelopes/genericenvelopes.go
{
"/status": {
"get": {
"description": "Returns the service status wrapped in the generic envelope. The response is\ndeclared as the doc-only StatusEnvelope, so data is the concrete StatusReport\nrather than an open schema.",
"tags": [
"status"
],
"operationId": "getStatus",
"responses": {
"200": {
"description": "the status envelope",
"schema": {
"$ref": "#/definitions/StatusEnvelope"
}
}
}
}
},
"/users/{id}": {
"get": {
"tags": [
"users"
],
"summary": "Returns a user wrapped in the same envelope, specialised to UserSummary.",
"operationId": "getUser",
"responses": {
"200": {
"description": "the user envelope",
"schema": {
"$ref": "#/definitions/UserEnvelope"
}
}
}
}
}
}
Full source: docs/examples/shaping/genericenvelopes/testdata/paths.json
Info
The handler never changes. Your code keeps returning the generic
APIResponse; the doc-only structs exist only to give the scanner a concrete
shape. They are valid Go, though — because the shadowing Data wins,
StatusEnvelope{} marshals to exactly the same JSON the generic envelope would,
so you may return one directly if you prefer a typed handler.
When embedding doesn’t fit
If your envelope has fields you would rather not promote — or you want the
documented type to live behind a reusable swagger:response — restate the
fields explicitly instead of embedding:
This produces the same concrete data, at the cost of repeating every field. The
embed-and-shadow form above is preferred whenever the envelope’s other fields map
through unchanged.
Composing a body from several models
A related need is wrapping a response with an extra payload — say a domain
model plus an auth token — rather than specialising one open field. Embed the
parts with swagger:allOf
and the body renders as the union of their $refs, with no open field at all:
// AuthToken is an extra payload some responses wrap alongside the domain model.
//
// swagger:model
type AuthToken struct {
Token string `json:"token"`
}
// LoginResult composes UserSummary with AuthToken via swagger:allOf, so the
// response body is the union of both models — a way to wrap a response with an
// added payload without an open Data field. The body renders as
// allOf:[{$ref:UserSummary},{$ref:AuthToken}].
//
// swagger:model
type LoginResult struct {
// swagger:allOf
UserSummary
// swagger:allOf
AuthToken
}Full source: docs/examples/shaping/genericenvelopes/genericenvelopes.go
{
"description": "LoginResult composes UserSummary with AuthToken via swagger:allOf, so the\nresponse body is the union of both models — a way to wrap a response with an\nadded payload without an open Data field. The body renders as\nallOf:[{$ref:UserSummary},{$ref:AuthToken}].",
"allOf": [
{
"$ref": "#/definitions/UserSummary"
},
{
"$ref": "#/definitions/AuthToken"
}
],
"x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/genericenvelopes"
}
Full source: docs/examples/shaping/genericenvelopes/testdata/compose.json
Point a response body at LoginResult, or embed the same swagger:allOf fields
directly in an in:body field to compose the allOf inline on the response
schema.