The Java API
typecast-java is a first-class facade, not a wrapper afterthought: records, sealed interfaces,
and Jakarta Bean Validation annotations, with no Scala types leaking through the API.
import typecast.javaapi.TypeCastClient;
TypeCastClient tc = TypeCastClient.builder()
.transport(transport) // any typecast.contract.SyncTransport
.model("claude-haiku-4-5") // required — model names are provider-scoped
.retry(3) // targeted reprompts, 3 attempts total
.build();
Invoice inv = tc.extract(Invoice.class, text); // throws TypedExtractionException on failure
Builder options: retry(maxAttempts), retry(maxAttempts, escalateModel, escalateAfter) (cheap
model first, escalate after N failures), maxTotalTokens(n) (hard budget per extraction).
Instances are immutable and as thread-safe as their transport.
Domain types: records and sealed interfaces
Schemas are derived at runtime by reflection and cached per class:
public sealed interface SupportAction permits Refund, Escalate, Reply {}
public record Refund(String orderId, @Positive BigDecimal amount) implements SupportAction {}
public record Escalate(@NotBlank String team, String reason) implements SupportAction {}
public record Reply(String message) implements SupportAction {}
- Records → objects: components become fields; component types may be records, enums,
List<T>,Map<String, T>,Optional<T>(optional field),BigDecimal,java.timetypes, and primitives/boxed primitives. - Sealed interfaces of records → tagged unions, discriminated on
"type"with the simple class name as the tag. The same fuzzy tag matching and discriminator inference as Scala unions applies on the way back. - Enums → closed string enums, fuzzy-matched at alignment.
- Unsupported shapes fail with a descriptive
IllegalArgumentExceptionat first use.
Dispatch is exhaustive. On Java 21+ use the pattern switch:
return switch (tc.extract(SupportAction.class, ticket)) {
case Refund r -> process(r);
case Escalate e -> route(e);
case Reply m -> send(m);
}; // no default needed: the compiler knows the sealed hierarchy is covered
On Java 17, pattern switch over sealed types still requires --enable-preview, so use the
equivalent instanceof pattern chain (final Java 17 syntax, no flags) — the sealed hierarchy
still guards the decision space at the type level:
SupportAction action = tc.extract(SupportAction.class, ticket);
if (action instanceof Refund r) return process(r);
else if (action instanceof Escalate e) return route(e);
else if (action instanceof Reply m) return send(m);
throw new IllegalStateException("unreachable: sealed");
Constraints: Jakarta Bean Validation
Annotations on record components lower to the same internal constraint IR as Scala's Iron refinements — driving schema keywords on the wire, post-hoc validation, and targeted retries:
| Jakarta annotation | IR | Notes |
|---|---|---|
@Min(n) / @Max(n) |
MinNumber(n) / MaxNumber(n) |
inclusive, per Jakarta semantics |
@Positive / @Negative |
exclusive MinNumber(0) / MaxNumber(0) |
|
@PositiveOrZero / @NegativeOrZero |
inclusive MinNumber(0) / MaxNumber(0) |
|
@DecimalMin / @DecimalMax |
numeric bounds | inclusive() respected |
@Size(min, max) |
MinLength/MaxLength on strings, MinItems/MaxItems on lists |
|
@Pattern(regexp) |
Pattern(regexp) |
flags() ignored (not expressible in the IR) |
@Email |
Format(email) |
|
@NotBlank |
MinLength(1) |
approximation: whitespace-only still passes the wire keyword; full check at validation |
@NotEmpty |
MinLength(1) on strings, MinItems(1) on lists |
A violated constraint is never visible to your code: it becomes a JSONPath-addressed validation error and a targeted reprompt, exactly as in Scala.
Failure handling
// Throwing style
try {
Invoice inv = tc.extract(Invoice.class, text);
} catch (TypedExtractionException e) {
log.warn(e.getMessage()); // rendered, JSONPath-addressed error
log.debug(e.report()); // the full attempt-by-attempt transcript
}
// Non-throwing style
ExtractionResult<Invoice> result = tc.extractWithReport(Invoice.class, text);
if (result.isSuccess()) {
Invoice inv = result.value().orElseThrow();
} else {
String why = result.errorMessage();
}
String transcript = result.report(); // always available — the eval seam
int attempts = result.attempts();
Jackson interop
For codebases with existing Jackson models, JacksonBridge converts both directions between
TypeCast's internal Js AST and JsonNode (numbers exchanged as BigDecimal, losslessly):
Transports from Java
Any typecast.contract.SyncTransport works — the Anthropic/OpenAI/Ollama transports from the
Scala modules, or your own implementation (one method: send). Java-friendly factory methods
and the LangChain4j adapter (typecast-langchain4j, reusing your app's existing ChatModel)
are in progress for the launch.
Kotlin interop
Kotlin works through the same typecast.javaapi surface, with two verified findings
(pinned by tests in examples-realworld/kotlin-triage/):
@JvmRecord data class+ sealed interfaces fully satisfy the reflection contract on JVM target 17+ — union derivation and"type"discrimination work unchanged.- Jakarta constraints need explicit use-site targets in Kotlin: write
@field:Positive, not@Positive. Kotlin's default annotation target is the constructor parameter, which record reflection never surfaces — a plain@Positiveis silently ignored.
See examples-realworld/kotlin-triage/ for a complete Gradle project consuming the
published artifacts.