HTTP problem details¶
The problem package writes the validation errors as the problem details of RFC 9457 (it obsoletes RFC 7807): an application/problem+json response with the errors of the fields in the errors member.
v := valigo.New(valigo.WithErrorsTransformer(problem.Transformer()))
if errs := v.Validate(r.Context(), &req); errs != nil {
problem.From(errs).Write(w) // 422 Unprocessable Content
return
}
problem.Transformer(opts...)makesValidatereturn one error, the*problem.Detailsof all errors;problem.From(errs)gets it back,problem.New(errs)builds it from the errors ofValidateTyped.Details.Write(w)writes the status and the JSON with theapplication/problem+jsoncontent type.- Every error of a field has the translated message
detail, the JSON Pointerpointerof RFC 6901 (built from the location:tags[1]is#/tags/1), thelocation, and thecodeandparamsfor the clients that translate the messages themselves. - Options:
WithType(the URI of the problem type,about:blankwhen it is empty),WithTitle("Validation failed" by default),WithStatus(422 by default),WithDetail.
Use JSON locations, so the pointers point to the fields of the request, see Errors.
Example¶
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"github.com/insei/fmap/v3"
"github.com/insei/valigo"
"github.com/insei/valigo/problem"
"github.com/insei/valigo/rules/str"
"github.com/insei/valigo/translator"
)
type SignUp struct {
Email string `json:"email"`
Tags []string `json:"tags"`
}
func main() {
v := valigo.New(
valigo.WithFieldLocationNamingFn(func(f fmap.Field) string { return f.GetTagPath("json", false) }),
valigo.WithErrorsTransformer(problem.Transformer(problem.WithType("https://example.com/problems/validation"))),
)
valigo.Configure[SignUp](v, func(b valigo.Configurator[SignUp], s *SignUp) {
b.Field(&s.Email).As(str.Of).Required().Email()
b.Slice(&s.Tags).As(str.Slice).MaxLen(2).Each(func(tag *str.BaseConfigurator) { tag.Alphanumeric() })
})
handler := translator.NewAcceptLanguageMiddleware()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req SignUp
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if errs := v.Validate(r.Context(), &req); errs != nil {
_ = problem.From(errs).Write(w)
return
}
w.WriteHeader(http.StatusCreated)
}))
r := httptest.NewRequest(http.MethodPost, "/sign-up", strings.NewReader(`{"email": "ann", "tags": ["go", "a b", "api"]}`))
r.Header.Set("Accept-Language", "ru")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
var body bytes.Buffer
_ = json.Indent(&body, w.Body.Bytes(), "", " ")
fmt.Println(w.Code, w.Header().Get("Content-Type"))
fmt.Print(body.String())
}
Output:
422 application/problem+json
{
"type": "https://example.com/problems/validation",
"title": "Validation failed",
"status": 422,
"errors": [
{
"detail": "Должно быть электронным адресом",
"pointer": "#/email",
"location": "email",
"code": "string.email"
},
{
"detail": "Не может содержать больше 2 элементов",
"pointer": "#/tags",
"location": "tags",
"code": "slice.max_len",
"params": {
"max": 2
}
},
{
"detail": "Должно содержать только латинские буквы и цифры",
"pointer": "#/tags/1",
"location": "tags[1]",
"code": "string.alphanumeric"
}
]
}
The title isn't translated: set it by WithTitle, e.g. with a translator of your messages.