# ADR-003: Generic DoJSON[T] Helper for Typed JSON Requests **Status:** Accepted **Date:** 2026-03-18 ## Context Calling an HTTP API and decoding the JSON response into a Go struct involves the same boilerplate in every caller: execute the request, check the status code, read the body, unmarshal into the target type. Without a shared helper, this boilerplate is duplicated across services and each copy is a potential source of inconsistent error handling. Prior to generics (Go 1.18), a typed helper required either an `interface{}` argument (losing type safety) or code generation. ## Decision A generic top-level function `DoJSON[T any]` is provided in `helpers.go`: ```go func DoJSON[T any](ctx context.Context, client Client, req *http.Request) (*T, error) ``` It: 1. Attaches `ctx` to the request via `req.WithContext(ctx)`. 2. Calls `client.Do(req)` — benefits from retry and circuit breaking. 3. Returns `MapStatusToError` for any HTTP 4xx or 5xx response. 4. Reads and unmarshals the body into a `T`, returning `*T` on success. 5. Wraps read and unmarshal failures in `xerrors.ErrInternal`. `MapStatusToError` maps common HTTP status codes to canonical `xerrors` codes: - 404 → `ErrNotFound` - 400 → `ErrInvalidInput` - 401 → `ErrUnauthorized` - 403 → `ErrPermissionDenied` - 409 → `ErrAlreadyExists` - 429 → `ErrUnavailable` - everything else → `ErrInternal` `DoJSON` is a free function, not a method, so it works with any value that satisfies the `Client` interface (including mocks). ## Consequences **Positive:** - Callers get a fully typed response with one function call and consistent error semantics. - The generic type parameter means no type assertions at the call site. - `MapStatusToError` is exported and reusable independently of `DoJSON`. **Negative:** - `DoJSON` always reads the entire body into memory before unmarshalling. Large response bodies should use `client.Do` directly with streaming JSON decoding. - The function returns `*T` (pointer to decoded value). Callers must nil-check when the response might legitimately be empty (though an empty body would typically produce an unmarshal error). - Response headers are not accessible through `DoJSON`; callers that need headers (e.g. for pagination cursors) must call `client.Do` directly.