Response Models

A handler has one return type, and that type describes one outcome: the one where nothing went wrong. A route that answers 201 with the thing it just created, 400 with a complaint about the request and 409 with a conflict report is describing three different shapes, and only the first of them is in the signature.

Status, WithResponseDoc and WithResponseModel are how the other two reach the document. None of them changes anything at run time. The success body is still enforced by the compiler; these describe what the route already does for everything else.

The status a success is written with

r.Post("/users/", handlers.CreateUser, muzak.Status(http.StatusCreated))

Status is the code written when the handler returns without an error. It defaults to 200, and it is part of the route because it never varies: a creating route creates every time it succeeds.

A status that depends on what happened is decided in the handler instead.

func CreateItem(ctx *muzak.Context, in schemas.ItemCreateIn) (schemas.ItemOut, error) {
    if in.Async {
        ctx.SetStatus(http.StatusAccepted)
    }
    return schemas.ItemOut{ID: in.ID}, nil
}

The two never compete: SetStatus wins when both are used, and the document describes the declared one. Declaring the run-time alternative is what the rest of this page is for.

A schema per status code

WithResponseModel[T] documents one status code and the model its body carries. The type argument is written exactly as a handler's Out type would be.

type UserOut struct {
    ID       int    `json:"id"`
    Username string `json:"username" doc:"The name the account signs in with"`
}

// ErrorOut is this service's own error body, which is not Muzak's envelope.
type ErrorOut struct {
    Message string `json:"message" doc:"What went wrong"`
    Code    string `json:"code" doc:"A machine-readable classifier"`
}

Each model is described once in the components section and referenced from every operation that names it, with the same doc tags, the same treatment of embedded structs and pointers, and the same well-known types a return type gets. Nothing about it is a second class of schema.

An empty description falls back to the status code's standard reason phrase, so the last line above reads as Internal Server Error in the document and in any page rendering it.

When the envelope is already right

A handler that reports a failure by returning an error produces Muzak's own error response. There is no model to choose, only an outcome to name.

r.Get("/items/{item_id}", handlers.ReadItem,
    muzak.WithResponseDoc(http.StatusNotFound, "The item does not exist"))

WithResponseDoc documents that status as ErrorResponse, the shape Error Handling covers, because that is what the route actually answers with. Reach for WithResponseModel when the body is something else: a legacy error shape kept for older clients, a partial result, or a body the handler writes itself.

Its description may be left empty too, so WithResponseDoc(http.StatusNotFound, "") is documented as Not Found.

No body, and bodies that are not JSON

A response model follows the same rules as a return type, including the two types that are not JSON at all.

ModelWhat is documented
a named structapplication/json, referencing the component
muzak.Emptythe status code, with no content
muzak.HTMLtext/html carrying a string
r.Get("/feed", handlers.Feed,
    muzak.WithResponseModel[muzak.Empty](http.StatusNotModified, "The feed has not changed"))

Declaring one for a whole router

WithResponseModel and WithResponseDoc are both router options as well as route options, so an outcome every route shares is declared once.

api := muzak.NewRouter(
    muzak.WithResponseModel[schemas.ErrorOut](http.StatusUnauthorized, "No usable credential was presented"))

api.Get("/items/{item_id}", handlers.ReadItem,
    muzak.WithResponseModel[schemas.ItemGoneOut](http.StatusGone, "The item was deleted"))

Every route beneath the router carries the 401, and the same works at the point of inclusion: app.Include(api, muzak.WithResponseModel[schemas.ErrorOut](418, "I'm a teapot")).

Declarations are applied outermost first, and the last one for a status code wins, so a route replaces what it inherited by declaring the same code again. That also means a declaration naming the status the route succeeds with replaces the response derived from the return type, which is deliberate for a route that writes its own body and a mistake otherwise.

A code outside 100 to 599 is not a status code, and fails the build rather than becoming a response nobody could receive.

What ends up in the document

For the creating route above:

"responses": {
  "201": {
    "description": "Created",
    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserOut"}}}
  },
  "400": {
    "description": "The request was malformed",
    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}
  },
  "422": {
    "description": "The request could not be validated.",
    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}
  },
  "500": {
    "description": "Internal Server Error",
    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}
  },
  "default": {
    "description": "An unexpected error occurred.",
    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}
  }
}

The 422 is added for you on every route that binds anything, since a request that fails validation never reaches the handler, and a default carrying the error envelope closes the list. Declaring 422 yourself replaces it, like any other code.

The documentation dashboard, if the application serves one, renders each of them as its own panel, with an example and an expandable schema per media type, so a client author can read the failure shapes without leaving the page.

Keep it honest

All of this is description. Nothing verifies at run time that a 404 really carries the model declared for it, so a declaration is a promise the handler has to keep. Document the outcomes the route already produces, and let the return type keep speaking for the one the compiler can check.

Where to go next

Responses covers the return type itself, headers, cookies and writing the response yourself. Error Handling covers the envelope WithResponseDoc describes, and OpenAPI covers the rest of the generated document.

Open source under MIT / Apache-2.0 · sustained by the people who ship on it.Built on net/http, and nothing else.