How to turn struct-tag validation failures into a standard "application/problem+json" response — the RFC 9457 format — with one method call, no hand-rolled error envelope. Most Go APIs invent their own validation error shape. One team returns {"errors": [...]}, another {"field_errors": {...}}, a third just a flat {"error": "message"} and hopes the client parses it. Every one of those is a private contract the client has to learn from your docs, because there's no shared shape for "here's what's wrong with your request." RFC 9457 — Problem Details for HTTP APIs — is the IETF standard that fixes this: a application/problem+json body with type, title, status, and room for problem-specific extensions. Checker now builds one of these directly from a failed struct validation, via CheckErrors.ProblemDetails(). The shape RFC 9457 defines four base members — type, title, status, detail, instance — and lets a specific problem type add its own. For validation errors, RFC 9457 §3.1 sketches exactly this extension: an invalid-params array listing which fields failed and why. That's what Checker produces. From a failed struct to a problem+json body Take a struct with a missing required field: type Person struct { Name string `checkers:"required"` } person := &Person{} errs, ok := checker.CheckStruct(person) if !ok { data, _ := json.Marshal(errs.ProblemDetails()) fmt.Println(string(data)) } { "type": "about:blank", "title": "Your request parameters failed validation.", "status": 400, "invalid-params": [ { "name": "Name", "reason": "Required value is missing.", "code": "REQUIRED" } ] } One method call — errs.ProblemDetails() — turns the same CheckErrors you'd otherwise call .JSON() on into a *ProblemDetails value, ready to marshal. type defaults to "about:blank" (RFC 9457's own default for "no more specific problem type registered"), status defaults to 400, and each invalid-params entry carries the field name, a localized human-readable reason, and the machine-readable code — the same code you'd branch on client-side with the plain .JSON() output. Wiring it into a handler The one thing RFC 9457 requires that a plain JSON body doesn't is the content type: func signupHandler(w http.ResponseWriter, r *http.Request) { var req SignupRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON", http.StatusBadRequest) return } errs, valid := checker.CheckStruct(&req) if !valid { pd := errs.ProblemDetails() w.Header().Set("Content-Type", "application/problem+json") w.WriteHeader(pd.Status) json.NewEncoder(w).Encode(pd) return } w.WriteHeader(http.StatusCreated) } pd.Status drives both the header and the body, so they can't drift out of sync. Overriding the defaults Type, Title, and Status are plain exported fields on the returned *ProblemDetails — RFC 9457 deliberately leaves them to the API producer, so Checker fills in reasonable defaults and gets out of your way. Set them before marshaling if you want a registered problem type or a different status code: pd := errs.ProblemDetails() pd.Type = "https://api.example.com/problems/validation-error" pd.Title = "Signup request failed validation" pd.Status = http.StatusUnprocessableEntity // 422 instead of 400 Nothing else about the call changes — invalid-params is still built from the same CheckErrors map. Localized problem details Since ProblemDetails() is just ProblemDetailsWithLocale(DefaultLocale) under the hood, a localized variant is one argument away — same 23 locales that back JSONWithLocale: pd := errs.ProblemDetailsWithLocale(locales.DeDE) The reason strings come back in German; type, status, and code are untouched, since those aren't meant for display. Why bother with a standard shape A hand-rolled error envelope works fine until you have more than one API, or a client library trying to handle errors generically, or an API gateway that wants to do something useful with 4xx bodies without special-casing your service. application/problem+json is already understood by tooling in several ecosystems — Spring, ASP.NET Core, and various API gateways emit or consume it natively — so producing it from Go costs you nothing extra (it's the same validation you were already running) and buys you a body shape other tools don't have to be taught. Try it go get github.com/cinar/checker/v2 import checker "github.com/cinar/checker/v2" ProblemDetails() and ProblemDetailsWithLocale() sit right next to JSON() and JSONWithLocale() on CheckErrors — pick whichever body shape your API needs, from the same validation call. Full details in the README.