📖 6 min read (~ 1200 words).

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:

The generic envelope
// 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

definitions[APIResponse]
{
  "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:

Doc-only envelope
// 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

definitions[StatusEnvelope]
{
  "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:

Generic — data is open
{
  "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

Doc-only — data is concrete
{
  "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
// 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 envelope

Full source: docs/examples/shaping/genericenvelopes/genericenvelopes.go

paths
{
  "/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:

// swagger:model
type StatusEnvelope struct {
    Status  string       `json:"status"`
    Data    StatusReport `json:"data"`
    Message string       `json:"message,omitempty"`
}

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:

swagger:allOf body
// 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

definitions[LoginResult]
{
  "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.