Date: 2026-09-19
Accepted
The project started as a single module hard-wired to Jackson 2 (ObjectMapper built directly
inside TypeSafeClient, @JsonTypeInfo/@JsonSubTypes on the DTOs) and to
java.net.http.HttpClient. We need to:
- support both Jackson 2 and Jackson 3 consumers, and other HTTP libraries (Apache HttpClient,
OkHttp, a mock transport for tests), without forking
TypeSafeClient's retry/backoff logic; - let the request/response DTOs be reused outside the HTTP client (e.g. to serialize the same payloads onto/from a Kafka topic) without pulling in a JSON or HTTP library at all.
Split into five Maven modules:
-
core —
TypeSafeClient,ApiToken,TypeSafeException, and the wire DTOsAnswer,Question,EvaluateRequest/EvaluateResponse,Usage,RequestId— all inio.github.dfa1.typesafe.core; plus two SPIs:JsonCodec(io.github.dfa1.typesafe.json) —writeValueAsBytes/readValue.HttpTransport(io.github.dfa1.typesafe.transport) —post/postAsync, the single HTTP callTypeSafeClientneeds.
corehas zero dependency on any JSON or HTTP library:TypeSafeClienttalks toHttpTransport/JsonCodec, never to a concrete library directly. Its onlyjava.netimport isURI, which every JDK ships. The DTOs carry no serialization annotations either. -
jdk-http-client — implements
HttpTransportwithJdkHttpTransport(java.net.http.HttpClient), discovered viaServiceLoaderthroughMETA-INF/services/io.github.dfa1.typesafe.transport.HttpTransport(or wired explicitly viaBuilder.httpTransport(...)). -
jackson2 / jackson3 — each implements
JsonCodec, discovered the same way throughMETA-INF/services/io.github.dfa1.typesafe.json.JsonCodec(orBuilder.jsonCodec(...)), and owns thetype-discriminator polymorphism forAnswer/Questionvia private Jackson mixins (addMixIn) instead of annotations on the DTOs themselves. -
bom — dependency-management POM listing the four artifacts above for consumers to import.
There is no separate client module: once TypeSafeClient depends only on the HttpTransport
abstraction rather than java.net.http directly, keeping it apart from core no longer buys
anything, so it lives with the DTOs and SPIs in core.
TypeSafeClient.Builder resolves both HttpTransport and JsonCodec via ServiceLoader at
build() time (or explicit Builder.httpTransport(...)/Builder.jsonCodec(...) overrides) —
the same pattern the JDK itself uses for java.sql.Driver or
java.nio.file.spi.FileSystemProvider.
corecan be depended on alone by any code that just needs to (de)serialize TypeSafe payloads (e.g. a Kafka producer), picking whicheverJsonCodecmodule fits its runtime — it still pulls inTypeSafeClient/ApiToken/HttpTransportunused, but harmlessly, sincecorehas zero transitive dependencies either way. Worth revisiting ifcoreever grows a real dependency of its own.- Adding a third JSON library or a different HTTP library later means adding one more codec or
transport module;
coredoesn't change. - A consumer that forgets a codec or transport module gets a clear
IllegalStateExceptionfromTypeSafeClient.Builder.build()at runtime, not aNoClassDefFoundError— deliberately a runtime check, not a compile-time one, since a compile-time check would mean picking one codec/transport as "the real dependency," which is exactly what this design avoids. - The polymorphism mixins in
jackson2/jackson3must be kept in sync by hand if a newAnswer/Questionsubtype is added — there's no compiler check tying the DTOs to the mixins. - A blocking-only HTTP library's
HttpTransport.postAsynchas no non-blocking send to chain off (unlikeJdkHttpTransport, which delegates straight toHttpClient.sendAsync) and has to fall back to runningposton a separate thread per call — a real cost under heavy concurrentevaluateAsyncuse that theHttpTransportinterface itself can't prevent.