Skip to content

Commit bac0a68

Browse files
committed
docs: Update README for TypeMapper and RequestHandler
1 parent e4c7516 commit bac0a68

1 file changed

Lines changed: 64 additions & 55 deletions

File tree

README.md

Lines changed: 64 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -26,45 +26,27 @@ It is designed to be simple to use while providing the essential features needed
2626

2727
### Basic Usage
2828
1. Create an OpenAPI specification file named `openapi.json` in your project resources.
29-
2. Define your HTTP handlers by implementing the `HttpHandler` interface:
29+
2. Define your handlers using the `RequestHandler` functional interface:
3030
``` java
31-
public class GetDataHandler implements HttpHandler {
32-
@Override
33-
public void handle(HttpExchange exchange) throws IOException {
34-
try (exchange) {
35-
byte[] bytes = """
36-
{
37-
"id": "some-id"
38-
}""".getBytes();
39-
40-
var responseHeaders = exchange.getResponseHeaders();
41-
responseHeaders.add("content-type", "application/json");
42-
43-
exchange.sendResponseHeaders(HTTP_OK, bytes.length);
44-
45-
try (var os = exchange.getResponseBody()) {
46-
os.write(bytes);
47-
}
48-
}
49-
}
50-
}
31+
// Inline lambda — returns JSON using the built-in Gson mapper.
32+
RequestHandler getDataHandler = req ->
33+
req.respond(200).json(Map.of("id", "some-id"));
5134

52-
public class PostDataHandler implements HttpHandler {
35+
// Class form — reads raw bytes or the pre-parsed body object.
36+
public class PostDataHandler implements RequestHandler {
5337
@Override
54-
public void handle(HttpExchange exchange) throws IOException {
55-
try (exchange) {
56-
// Access the raw request body bytes.
57-
byte[] body = Request.bytes(exchange);
58-
// Or get the already-parsed object (Map or List) produced by your JsonMapper.
59-
Object parsed = Request.parsed(exchange);
60-
61-
exchange.sendResponseHeaders(HTTP_OK, -1);
62-
}
38+
public void handle(Request request) throws IOException {
39+
// Access the raw request body bytes.
40+
byte[] body = request.bytes();
41+
// Or get the already-parsed object (Map / List) produced by the registered TypeMapper.
42+
Object parsed = request.parsed();
43+
44+
request.respond(200).json(parsed);
6345
}
6446
}
6547
```
6648

67-
3. Initialize the server (using Gson in this example):
49+
3. Initialize the server:
6850
``` java
6951
public class YourServerLauncher {
7052
public static void main(String[] args) throws Exception {
@@ -75,17 +57,13 @@ public class YourServerLauncher {
7557
Map<String, Object> raw = (Map<String, Object>) gson.fromJson(text, Map.class);
7658
Spec spec = Spec.from(raw);
7759

78-
// Body parser. Returns a Map for objects, List for arrays.
79-
JsonMapper mapper = body -> gson.fromJson(new String(body), Object.class);
80-
8160
// Handlers by operationId.
82-
Map<String, HttpHandler> handlers = new HashMap<>();
83-
handlers.put("get-data", new GetDataHandler());
61+
Map<String, RequestHandler> handlers = new HashMap<>();
62+
handlers.put("get-data", getDataHandler);
8463
handlers.put("post-data", new PostDataHandler());
8564

8665
var server = OpenApiServer.builder()
8766
.spec(spec)
88-
.jsonMapper(mapper)
8967
.handlers(handlers)
9068
.exceptionHandler(Handlers.defaultExceptionHandler())
9169
.build();
@@ -100,13 +78,51 @@ Map<String, Object> raw = new Yaml().load(Files.newInputStream(Path.of("openapi.
10078
```
10179
The rest is identical.
10280

81+
### JSON mapping
82+
83+
The library ships an internal `GsonJsonMapper` that is auto-registered for `application/json` when Gson is on the classpath and no user-supplied JSON mapper has been registered. It:
84+
85+
- Returns JSON integers as `Long` and fractional numbers as `Double`.
86+
- Writes JSR-310 types (`Instant`, `OffsetDateTime`, `ZonedDateTime`, `LocalDateTime`, `LocalDate`, `LocalTime`) as ISO-8601 strings.
87+
88+
For non-ISO date formats, custom naming strategies, or other custom serialization, register your own `TypeMapper`:
89+
90+
``` java
91+
var server = OpenApiServer.builder()
92+
.spec(spec)
93+
.bodyMapper("application/json", new MyCustomJsonMapper())
94+
.handlers(handlers)
95+
.build();
96+
```
97+
98+
If Gson is not on the classpath and no `application/json` mapper is registered, `build()` throws `IllegalStateException`.
99+
100+
### Body parsers and response writers
101+
102+
`TypeMapper` is the per-media-type read/write contract:
103+
104+
``` java
105+
public interface TypeMapper {
106+
Object readFrom(byte[] body, String contentTypeHeader);
107+
byte[] writeTo(Object value);
108+
}
109+
```
110+
111+
Register a custom mapper for any media type via `Builder.bodyMapper(mediaType, mapper)`. Built-in defaults:
112+
113+
- `application/x-www-form-urlencoded` — read-only. Produces `Map<String, Object>`. A single value is a `String`; repeated keys produce a `List`.
114+
- `text/plain` — read and write. Produces a decoded `String`; writes via `String.getBytes()`.
115+
- `application/json` — auto-registered when Gson is on the classpath (see above).
116+
117+
User-supplied mappers take precedence over built-in defaults, so you can override any of the above.
118+
103119
### Request body content types
104120

105-
The server reads `requestBody.content` from the spec and selects a parser by the request's media type (the bare `type/subtype` from `Content-Type`, e.g. `application/json`; lookup is case-insensitive):
121+
The server reads `requestBody.content` from the spec and selects a mapper by the request's media type (the bare `type/subtype` from `Content-Type`, e.g. `application/json`; lookup is case-insensitive):
106122

107123
| Content type | Parser | Coercion |
108124
| ------------------------------------- | ---------------------------------------------------------------------------- | -------- |
109-
| `application/json` | Caller-supplied `JsonMapper` | No — strict against the schema |
125+
| `application/json` | `GsonJsonMapper` (auto) or caller-supplied `TypeMapper` | No — strict against the schema |
110126
| `application/x-www-form-urlencoded` | Built-in. `Map<String, Object>`. A single value is a `String`; repeated keys produce a `List`. After coercion the element type tracks the schema (e.g. an `integer` array yields `List<Long>`). | Yes — field values coerced to the property schema type (integer / number / boolean / array of those) |
111127
| `text/plain` | Built-in. Decoded `String` | No — schema should be `type: string` |
112128

@@ -159,7 +175,6 @@ to OpenAPI parameter / body validation.
159175
``` java
160176
var server = OpenApiServer.builder()
161177
.spec(spec)
162-
.jsonMapper(mapper)
163178
.handlers(handlers)
164179
.addHandler("/alive", Handlers.aliveHandler())
165180
.addHandler("/schemas/v1/openapi.yaml",
@@ -189,7 +204,6 @@ try-with-resources) via the builder:
189204
```java
190205
try (var server = OpenApiServer.builder()
191206
.spec(spec)
192-
.jsonMapper(mapper)
193207
.handlers(handlers)
194208
.shutdownTimeoutSeconds(5) // close() drains up to 5s; default is 0
195209
.build()) {
@@ -202,20 +216,15 @@ try (var server = OpenApiServer.builder()
202216

203217
## Features
204218
- OpenAPI specification support
205-
- Automatic request body parsing for JSON arrays and objects
206-
- Custom HTTP handler support
207-
- Built on Java's native `HttpServer` with Thread-Per-Request behaviour using Virtual Threads.
208-
- Custom integration for JSON serialization/deserialization
219+
- Automatic request body parsing and response writing per media type via `TypeMapper`
220+
- `RequestHandler` functional interface — a single `handle(Request)` method replaces raw `HttpExchange` manipulation
221+
- Fluent `ResponseBuilder` via `request.respond(status)` with terminals: `empty()`, `bytes()`, `text()`, `json()`, `body()`, `stream()`
222+
- Built-in `GsonJsonMapper` auto-registered when Gson is on the classpath (no explicit wiring needed)
223+
- Built on Java's native `HttpServer` with Thread-Per-Request behaviour using Virtual Threads
209224

210225

211226
## Handler Registration
212-
Handlers are registered using string keys that correspond to your OpenAPI operation IDs.
213-
214-
215-
## JSON Mapping
216-
The library uses a flexible JSON mapping system that automatically detects and parses (using a mapper of choice):
217-
- JSON arrays (`[...]`)
218-
- JSON objects (`{...}`)
227+
Handlers are registered in a `Map<String, RequestHandler>` keyed by OpenAPI `operationId`.
219228

220229
## Local development
221230

@@ -235,7 +244,7 @@ A few things to know:
235244

236245
- **Single-process model.** No horizontal scaling primitives are bundled; run multiple instances behind a load balancer for production scale.
237246
- **JDK HttpServer is the throughput ceiling.** It's documented as a low-throughput / dev-test server. If you need to go materially above the rates above, deploy the same filter/validator/router stack on Jetty, Helidon Níma, or Netty — the spec and validation code is server-agnostic.
238-
- **Per-request state uses `ScopedValue`** (Java 25, JEP 506), not `HttpExchange.setAttribute`. This matters if a handler offloads work to an executor that's not a `StructuredTaskScope`-managed child thread: the `ScopedValue` is not visible there, so the handler must capture the values it needs (e.g. `byte[] body = Request.bytes();`) before submitting.
239-
- **`HttpExchange.sendResponseHeaders(rCode, length)` gotcha.** When a handler has no response body, pass `-1` (`Content-Length: 0`, no body); passing `0` produces a chunked response with zero chunks, which is technically non-conformant.
247+
- **Per-request state uses `ScopedValue`** (Java 25, JEP 506). This matters if a handler offloads work to an executor that's not a `StructuredTaskScope`-managed child thread: the `ScopedValue` is not visible there, so the handler must capture the values it needs (e.g. `byte[] body = request.bytes();`) before submitting.
248+
- **Empty responses must use `request.respond(status).empty()`**, which sends `responseLength = -1` (`Content-Length: 0`, no body). Passing `0` produces a chunked response with zero chunks, which is technically non-conformant.
240249

241250
## Known limitations or missing features

0 commit comments

Comments
 (0)