Skip to content

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 strict extract[A] result.
  • Tools → MCP from the same derives clause. derives LlmTool turns 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. extractWithReport returns an ExtractionReport — 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.
  • StreamingPartial[A] semantics and the last == strict guarantee.
  • Tools & MCP — typed tool dispatch and MCP export.
  • Java — the TypeCastClient facade 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