BindingError embeds *HTTPError but did not implement json.Marshaler, so
DefaultHTTPErrorHandler's type switch fell through to its default branch
(the value is a *BindingError, not a *HTTPError), flattening responses to
{"message":"Bad Request"} and dropping both the field name and the binder
message — a regression from fbfe216.
Implement MarshalJSON on *BindingError so it takes the handler's
json.Marshaler branch, restoring the structured {"field":...,"message":...}
response (the v4.10.2 behavior).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Problem (#2771)
A binding error returned from a handler is serialized as {"message":"Bad Request"} — the field name and the binder message are both lost.
Root cause
BindingError embeds *HTTPError but does not implement json.Marshaler. In DefaultHTTPErrorHandler the type switch runs on the HTTPStatusCoder extracted via errors.As, whose dynamic type is *BindingError. Go's case *HTTPError matches only the exact type, so a *BindingError falls through to the default branch → {"message": http.StatusText(code)}. Regression from fbfe216 (#2456).
Verified on current master:
GET /doc?docNum=abc → 400 {"message":"Bad Request"}Fix
Implement MarshalJSON on *BindingError so it takes the handler's existing case json.Marshaler branch (which is checked before *HTTPError). Restores the v4.10.2 structured response:
GET /doc?docNum=abc → 400 {"field":"docNum","message":"failed to bind field value to int"}This is the approach the maintainer outlined in the issue thread ("we could make echo.BindingError … implement json.Marshaler").
Test
TestBindingError_serializesToStructuredJSON (written first; fails on master with field=<nil>, message="Bad Request"). gofmt/vet clean; full root-package suite passes.
Fixes #2771.
🤖 Generated with Claude Code