Follow-up from the Zod-replacement epic. Stem received valibot + UI schemas + generated types + validator/v10, but did NOT get the matching Go-side strict JSON decode helper that seed#1131 introduced.
Gap
Seed has internal/api/decode.go exposing:
decodeJSONStrict(w, r, dst, max, logger) — strict mode (DisallowUnknownFields) + size limit, writes a standard JSON envelope error on failure
decodeJSONStrictLocalized(w, r, dst, max, logger, localizer) — same, with i18n error strings
decodeJSONStrictLocalizedWith(w, r, dst, max, logger, localizer, logAttrs...) — variant that carries extra structured log attrs (used by auth/MFA endpoints for client_ip, username, factor)
Stem's internal/api/ handlers currently roll their own json.Decoder + DisallowUnknownFields ad-hoc, which means:
- Inconsistent error envelope shapes across endpoints
- Easy to forget
MaxBytesReader on new handlers
- No localized error messages
Scope
- Create
internal/api/decode.go mirroring seed's three helpers (adjust for stem's existing logger/i18n/error envelope conventions).
- Sweep all
internal/api/handlers_*.go files; convert ad-hoc decoders to the new helpers.
- Preserve any handler that needs a non-envelope error shape — leave inline strict mode + size limit but keep the existing error path.
- Add
internal/api/decode_test.go matching seed's test coverage (strict mode rejects unknown fields, MaxBytesReader truncates, i18n variants render localized strings).
Out of scope
- Schema-driven DTO generation (already done via the json-schema pipeline).
- New endpoints; this is a pure refactor.
Acceptance
make lint && make test clean.
- No handler in
internal/api/ uses raw json.NewDecoder(r.Body) without going through a strict helper (grep gate in CI optional).
- Closes the seed/stem parity gap from the Zod-replacement epic.
Follow-up from the Zod-replacement epic. Stem received valibot + UI schemas + generated types + validator/v10, but did NOT get the matching Go-side strict JSON decode helper that seed#1131 introduced.
Gap
Seed has
internal/api/decode.goexposing:decodeJSONStrict(w, r, dst, max, logger)— strict mode (DisallowUnknownFields) + size limit, writes a standard JSON envelope error on failuredecodeJSONStrictLocalized(w, r, dst, max, logger, localizer)— same, with i18n error stringsdecodeJSONStrictLocalizedWith(w, r, dst, max, logger, localizer, logAttrs...)— variant that carries extra structured log attrs (used by auth/MFA endpoints for client_ip, username, factor)Stem's
internal/api/handlers currently roll their ownjson.Decoder+DisallowUnknownFieldsad-hoc, which means:MaxBytesReaderon new handlersScope
internal/api/decode.gomirroring seed's three helpers (adjust for stem's existing logger/i18n/error envelope conventions).internal/api/handlers_*.gofiles; convert ad-hoc decoders to the new helpers.internal/api/decode_test.gomatching seed's test coverage (strict mode rejects unknown fields, MaxBytesReader truncates, i18n variants render localized strings).Out of scope
Acceptance
make lint && make testclean.internal/api/uses rawjson.NewDecoder(r.Body)without going through a strict helper (grep gate in CI optional).