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.
| Model | What is documented |
|---|---|
| a named struct | application/json, referencing the component |
muzak.Empty | the status code, with no content |
muzak.HTML | text/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.