TypeCast
Make LLM output a compile-time concern.
TypeCast is a JVM library that turns untrusted LLM text into validated, typed values: schema derivation from your domain types, schema-aligned repair, constraint-aware retry, and streaming partial decode. One library owns the trust boundary between any LLM provider and your program — for Scala 3 & Java on the JVM, JDK 17+.
your domain type ──derive──▶ schema ──▶ provider ──▶ raw text
│
typed value ◀── validate ◀── repair ◀── parse ◀──────┘
▲ │
└──── targeted retry ◀─────┘ (on failure, with precise errors)
Hello, world
Declare your domain type, derive the schema, extract. No hand-written JSON Schema, no prompt
begging for JSON, no JsonNode:
import java.time.LocalDate
import typecast.*
import typecast.anthropic.Anthropic
case class LineItem(description: String, quantity: Int, amount: BigDecimal) derives LlmSchema
case class Invoice(
@desc("legal name of the issuing company") vendor: String,
invoiceNumber: String,
invoiceDate: LocalDate,
dueDate: Option[LocalDate], // optional field, not a null union
total: BigDecimal,
lineItems: List[LineItem]
) derives LlmSchema
val tc = TypeCast(Anthropic.fromEnv(), model = "claude-haiku-4-5")
tc.extract[Invoice](emailBody) match
case Right(inv) => println(s"${inv.vendor}: ${inv.total} due ${inv.dueDate.getOrElse("on receipt")}")
case Left(err) => println(err.render) // JSONPath-addressed, human-readable
extract[A] returns a validated A or a typed failure explaining exactly what went wrong —
never a half-parsed value. BigDecimal, java.time types, nested lists, and optional fields all
decode losslessly; provider-native structured output is used automatically when available.
Why it exists
On the Python/TypeScript side, Instructor and BAML define the category: derive a schema from a type, repair almost-right output, retry on validation failure. On the JVM, LangChain4j and Spring AI offer structured output, but as a thinner, reflection-based feature without the repair / retry / streaming machinery. Instructor has no JVM port, and BAML reaches the JVM only through an OpenAPI bridge.
TypeCast is the JVM-native answer to that gap, addressed to the whole JVM — Scala 3 and Java, JDK 17+ — rather than to Scala alone. It owns one layer: the trust boundary between an LLM provider and your program. It is not an agent framework, a prompt library, or a RAG stack. See How TypeCast compares for the honest, layer-by-layer breakdown.
What you get
- Repair before retry. Deterministic repair is free; retries cost tokens. The tolerant
parser strips code fences, removes trailing commas, and completes truncations; alignment
fuzzy-matches keys and enum values and applies unambiguous coercions (
"67"→67) — all before a single reprompt is spent, and all recorded in the report. - Targeted reprompts. A validation failure becomes a reprompt that pins the already-valid fields and asks the model to fix only the failing paths, with optional model escalation and hard token budgets.
- Capability lowering + post-hoc backstop. Each provider declares what it can enforce natively; the schema is lowered to that subset and every constraint that cannot travel on the wire is enforced by validation instead, feeding the same retry loop. User code never changes across providers.
- Streaming partial decode.
Partial[A]values arrive as tokens do: each already-complete field is already validated, and the final element is guaranteed identical to the strictextract[A]result. - Tools → MCP from the same
derivesclause.derives LlmToolturns a case class into a tool definition; malformed tool arguments get the same repair / retry treatment as extractions;McpServer.from(tools).serveStdio()exports the registry as an MCP server. - First-class Java API. Records, sealed interfaces, and Jakarta Bean Validation annotations
(
@Positive,@Pattern,@Email) drive the same constraint → validate → targeted-retry loop. - Every extraction observable.
extractWithReportreturns anExtractionReport— attempts, repairs, alignments, errors, token usage — the seam for evals and CI gates.
Pre-1.0
TypeCast is pre-1.0: organization io.typecast, version 0.1.0-SNAPSHOT, not yet on Maven
Central. The API surface is small but may still move, and the TypeCast name is a working
title. Published benchmark numbers are in progress.
Next steps
Start here
- Installation — coordinates for sbt, Gradle, and Maven; the module table; provider setup.
- Getting started — install → extract → report → retry config → provider swap.
- Architecture — how the pipeline fits together.
Guides
- Structured output — derivation,
@desc, unions, enums, Iron & Jakarta constraints. - Retry & repair — the pipeline stages and real report transcripts.
- Streaming —
Partial[A]semantics and thelast == strictguarantee. - Tools & MCP — typed tool dispatch and MCP export.
- Java — the
TypeCastClientfacade for Java, Kotlin, and Spring. - Providers & lowering — the capability model and per-provider enforcement.
Reference
- How TypeCast compares — versus LangChain4j, Spring AI, Instructor, BAML.
- FAQ — common questions and gotchas.
- Examples — the capability examples and the real-world apps.
- Benchmarks — the harness, tiers, and methodology.
- Lowering matrix — per-mode constraint and structure support.
Internals
- Partial derivation spike — the streaming derivation design notes.