You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/superpowers/specs/2026-05-13-type-mapper-request-handler-design.md
+30-4Lines changed: 30 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -28,7 +28,7 @@ read/write per media type, and `RequestHandler` for handlers that receive a
28
28
In scope:
29
29
30
30
-`TypeMapper` interface (read + write) and per-media-type registration on the builder.
31
-
- Delete `JsonMapper`; the user supplies a `TypeMapper` for `application/json` instead. Default form and text mappers wired automatically.
31
+
- Delete `JsonMapper`; the user supplies a `TypeMapper` for `application/json` instead. Default form and text mappers wired automatically. Optional Gson-backed default for `application/json` activated when Gson is on the classpath and no user-supplied JSON mapper is registered.
32
32
- New `RequestHandler` interface; `handlers(...)` builder method changed to `Map<String, RequestHandler>` (breaking).
33
33
-`Request` repurposed from a static-accessor utility into the per-request handle handlers receive. Read API mirrors today's `RequestContext`; adds a response gateway with one-shot and streaming terminals.
34
34
- Internal `RequestContext` record and public `Request.CONTEXT``ScopedValue` removed.
@@ -61,10 +61,35 @@ public interface TypeMapper {
61
61
62
62
-`application/x-www-form-urlencoded` — built-in form mapper. `readFrom` parses to `Map<String,Object>`. `writeTo` throws `UnsupportedOperationException`; form-encoded responses are unusual and we won't speculate on the encoding until someone needs it.
63
63
-`text/plain` — built-in text mapper. `readFrom` decodes bytes using the charset declared on `Content-Type` (default UTF-8). `writeTo` returns `String.valueOf(value).getBytes(UTF_8)`.
64
-
-`application/json` — **no default**; the user must supply a `TypeMapper`. Mirrors the current contract that `jsonMapper(...)`is required.
64
+
-`application/json` — **no static default**; if the user does not register a mapper, the builder probes the classpath for Gson and falls back to a built-in Gson-backed mapper (see below). If Gson is not on the classpath either, `build()`fails with the same "no JSON mapper registered" error.
65
65
66
66
Lookup: case-insensitive on the media-type subtype (existing `ContentTypeHeader.mediaType` already lowercases).
67
67
68
+
### Optional Gson fallback for `application/json`
69
+
70
+
To shrink setup for callers that already use Gson, the library ships an internal Gson-backed `TypeMapper` and auto-registers it when:
71
+
72
+
1. The builder reaches `build()` and no `TypeMapper` has been registered for `application/json`; and
73
+
2.`com.google.gson.Gson` is resolvable on the classpath.
74
+
75
+
Implementation:
76
+
77
+
- Gson is an **optional** Maven dependency (`<optional>true</optional>` / `provided`). The library does not pull Gson into consumer classpaths.
78
+
- One internal class — `com.retailsvc.http.internal.gson.GsonJsonMapper` — imports Gson directly. The builder loads it reflectively (`Class.forName(...)`) only after probing for Gson, so consumers without Gson never trigger class-loading of that adapter and never see `NoClassDefFoundError`.
79
+
- Jackson is **not** auto-detected. Jackson users register a `TypeMapper` explicitly. Auto-providing a default `ObjectMapper` would pick the wrong configuration for most Jackson users (modules, naming, date formats).
80
+
81
+
Number handling on read:
82
+
83
+
- Gson's default `fromJson(json, Object.class)` deserialises every JSON number as `Double`. The library's validator has `IntegerSchema`, format-width checks, and NaN/Infinity rejection that assume integral values arrive as `Long`/`Integer`. To avoid surprises, `GsonJsonMapper` is constructed with a custom `TypeAdapter<Object>` that:
84
+
- reads integral JSON numbers (no fraction, no exponent producing a fraction) into `Long`;
85
+
- reads non-integral or out-of-`Long`-range numbers into `Double`;
86
+
- reads everything else (`String`, `Boolean`, `null`, arrays, objects) the way Gson's default does.
87
+
- This is a well-known Gson pattern; ~30 lines, tested in isolation.
88
+
89
+
Number handling on write — known caveat, documented in README:
90
+
91
+
-`GsonJsonMapper.writeTo(value)` calls `gson.toJson(value)` on a default `Gson` instance and returns UTF-8 bytes. This handles `Map<String,Object>`, `List<?>`, `String`, `Number`, `Boolean`, and `null` cleanly. It does **not** serialise `java.time.Instant`, `LocalDate`, or other JSR-310 types in a useful way (Gson's default emits internal field values). Callers that return such types from handlers — or want any non-default serialization — must register their own `TypeMapper`. The fallback is intended for the "I'm already using Gson and the defaults are fine" case.
92
+
68
93
### `Request`
69
94
70
95
`com.retailsvc.http.Request` becomes the per-request handle. Concrete final class (no interface — YAGNI; extract later if testability demands it).
@@ -187,7 +212,8 @@ This is a pre-1.0 library; breaking changes are acceptable.
187
212
188
213
Existing integration tests (`*IT.java`) exercise the full stack and will be updated to use the new handler signature. Unit tests cover:
189
214
190
-
-`TypeMapper` registration: defaults wired, user overrides win, missing `application/json` mapper fails the builder.
215
+
-`TypeMapper` registration: defaults wired, user overrides win, missing `application/json` mapper fails the builder when Gson is not on the classpath.
216
+
- Gson fallback: with Gson on the classpath and no user JSON mapper, `build()` succeeds and `application/json` round-trips via `GsonJsonMapper`. Integer-preserving `TypeAdapter` returns `Long` for integral numbers and `Double` for fractional / out-of-range numbers. With Gson absent and no user JSON mapper, `build()` fails with the existing error.
191
217
- Built-in text mapper: round-trip via `readFrom` and `writeTo`; charset handling.
192
218
- Built-in form mapper: `readFrom` parses; `writeTo` throws `UnsupportedOperationException`.
@@ -199,6 +225,6 @@ Existing integration tests (`*IT.java`) exercise the full stack and will be upda
199
225
200
226
The implementation plan will sequence this as:
201
227
202
-
1. Introduce `TypeMapper`; convert form and text built-ins to implement it; delete `JsonMapper`; switch the builder to `bodyMapper(String, TypeMapper)`; rewire `RequestPreparationFilter` to use the registered mappers and drop the hardcoded media-type switch.
228
+
1. Introduce `TypeMapper`; convert form and text built-ins to implement it; add the internal `GsonJsonMapper` (Gson as optional Maven dependency) and the builder's classpath-probe fallback; delete `JsonMapper`; switch the builder to `bodyMapper(String, TypeMapper)`; rewire `RequestPreparationFilter` to use the registered mappers and drop the hardcoded media-type switch.
203
229
2. Move form-coercion out of `FormUrlEncodedParser` into the validator path.
204
230
3. Build the new `Request` class (read API + response gateway), the internal `ScopedValue<Request>` handoff, and the `RequestHandler` interface; switch `handlers(...)` to `Map<String, RequestHandler>`; update example launcher and tests; delete the static `Request` accessors, the public `ScopedValue`, and the `RequestContext` record.
0 commit comments