RFC-7807 Problem JSON structure in Go
problem is a single source of truth for an RFC-7807 application/problem+json structure, use it whenever you need to return an error from a REST API and your
APIs will all be consistent 👌🏻
Warning
This module requires the Go 1.25 jsonv2 experiment: GOEXPERIMENT=jsonv2 in order to inline the Extra map when serializing
See https://go.dev/blog/jsonv2-exp#experimenting-with-jsonv2
go get go.followtheprocess.codes/problem@latestWhenever you need a problem:
prob := problem.Problem{
Type: "https://example.com/probs/out-of-credit",
Title: "Not enough credit",
Detail: "Your current balance is 30, but that costs 50",
Instance: "/account/12345/msgs/abc",
Status: http.StatusBadRequest,
}Or you can use the New function with a bunch of options if you like:
prob := problem.New(
problem.Type("https://example.com/probs/out-of-credit"),
problem.Title("Not enough credit"),
problem.Detail("Your current balance is 30, but that costs 50"),
problem.Instance("/account/12345/msgs/abc"),
problem.Status(http.StatusBadRequest),
)And these will both serialize to the following JSON:
{
"type": "https://example.com/probs/out-of-credit",
"title": "Not enough credit",
"detail": "Your current balance is 30, but that costs 50",
"instance": "/account/12345/msgs/abc",
"status": 400
}RFC-7807 allows for arbitrary additional fields in this response to convey any additional context your error might have. Using problem you do this with the Extra map:
prob := problem.Problem{
Type: "https://example.com/probs/out-of-credit",
Title: "Not enough credit",
Detail: "Your current balance is 30, but that costs 50",
Instance: "/account/12345/msgs/abc",
Status: http.StatusBadRequest,
Extra: map[string]any{
"balance": 30,
"accounts": []string{"/accounts/12345", "/accounts/67890"},
},
}Or with the New pattern:
prob := problem.New(
problem.Type("https://example.com/probs/out-of-credit"),
problem.Title("Not enough credit"),
problem.Detail("Your current balance is 30, but that costs 50"),
problem.Instance("/account/12345/msgs/abc"),
problem.Status(http.StatusBadRequest),
problem.Extra("balance", 30),
problem.Extra("accounts", []string{"/accounts/12345", "/accounts/67890"}),
)These will both serialize to the following JSON:
{
"type": "https://example.com/probs/out-of-credit",
"title": "Not enough credit",
"detail": "Your current balance is 30, but that costs 50",
"instance": "/account/12345/msgs/abc",
"status": 400,
"balance": 30,
"accounts": [
"/accounts/12345",
"/accounts/67890"
]
}Tip
There is also an ExtraMap to allow adding a whole map of extra variables in one go rather than one at a time as shown above
The package also provides some helpers for use in HTTP services to quickly respond with a problem:
package main
import (
"net/http"
"go.followtheprocess.codes/problem"
)
func Bang(w http.ResponseWriter, r *http.Request) {
problem.Respond(
w,
problem.Title("Uh oh"),
problem.Detail("A thing went wrong"),
problem.Status(http.StatusBadRequest),
)
}
func main() {
http.HandleFunc("/", Bang)
http.ListenAndServe(":8080", nil)
}Respond will:
- Set the
Statuscode on the response - Write the
Content-Typeheader asapplication/problem+json - Marshal the
Problemas JSON to thehttp.ResponseWriter- If that fails, a default problem is written instead
This package was created with copier and the FollowTheProcess/go-template project template.