DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Designing a Production-Grade Multi-Agent LLM Architecture for Structured Data Extraction
  • Token-Efficient APIs for the Agentic Era
  • Fine-Tune SLMs for Free: From Google Colab to Ollama in 7 Steps
  • Talk to Your BigQuery Data Using Claude Desktop

Trending

  • Dynamic Arrays, Spill, and LET: What Changed in Excel and Why It Matters for Java Applications
  • Building a Zero-Cost Daily Job Alert Pipeline on GitHub Actions
  • Ampere PMU Profiler: A Guide to Microarchitecture Profiling
  • Your Spark Job Isn't Slow Because of Bad Code. It's Slow Because of the Wrong Join
  1. DZone
  2. Coding
  3. Languages
  4. When TypeScript Types Meet Untrusted AI: Building Type-Safe LLM Pipelines With Runtime Validation

When TypeScript Types Meet Untrusted AI: Building Type-Safe LLM Pipelines With Runtime Validation

Learn in this article how to treat LLM output as unknown until runtime schema validation proves it safe for typed application logic.

By 
Bhanu Sekhar Guttikonda user avatar
Bhanu Sekhar Guttikonda
DZone Core CORE ·
Sep. 11, 26 · Analysis
Likes (0)
Comment
Save
Tweet
Share
109 Views

Join the DZone community and get the full member experience.

Join For Free

TypeScript can make an LLM integration look safer than it is. A function may promise Promise<Classification> and every branch may compile under strict settings, yet none of those guarantees prove that a model returned a valid Classification. TypeScript annotations are erased during compilation and do not alter runtime behavior, so data crossing an AI boundary remains untrusted until executable validation proves otherwise. 

The practical goal is a pipeline in which model output becomes domain data only after passing a runtime contract. 

Static Types Stop at the Model Boundary

A type assertion immediately after JSON parsing suppresses compiler uncertainty without establishing any runtime fact. Parsed data can contain missing fields, unexpected strings, invalid ranges, or extra properties, while an assertion simply tells TypeScript to accept the declared shape. Because unknown requires narrowing before operations are permitted, it is the safer representation for an untrusted boundary. 

TypeScript
 
type Classification = {
  label: "bug" | "feature" | "question";
  confidence: number;
};

const candidate: unknown = JSON.parse(raw);

const result = candidate as Classification;


The final line creates a compile-time claim with no runtime check. Risk rises when the result controls database writes, tool calls, or authorization-sensitive workflows. A reliable boundary keeps the value as unknown until validation establishes the required structure. 

Make the Schema the Executable Contract

A runtime schema library closes the gap between erased TypeScript types and actual JavaScript values. Zod is designed to define runtime schemas while inferring static TypeScript types from the same definition, which allows one artifact to serve both validation and compile-time ergonomics. z.strictObject() is especially useful at an LLM boundary because unexpected keys become validation failures rather than silently extending the accepted surface.

TypeScript
 
const ClassificationSchema = z.strictObject({
  label: z.enum(["bug", "feature", "question"]),
  confidence: z.number().min(0).max(1),
  rationale: z.string().min(1).max(800),
});

type Classification = z.infer<typeof ClassificationSchema>;


The schema carries runtime constraints that a TypeScript type alone cannot enforce. The enum limits labels, numeric checks enforce the confidence interval, string bounds constrain explanations, and strict object handling rejects undeclared fields. The inferred Classification type follows the schema instead of being maintained separately, reducing static/runtime drift. Zod documents z.infer for static inference and structured errors for failed parses. 

Parse Before Data Enters Domain Logic

Validation works best when it is treated as a boundary operation rather than scattered defensive checks. Raw model text first has to satisfy JSON syntax, then the resulting JavaScript value has to satisfy the runtime schema. Only the successfully parsed value should enter business logic. safeParse() returns a discriminated result that contains either validated data or a ZodError, which makes rejection paths explicit without using exceptions for normal validation flow.

TypeScript
 
function parseClassification(raw: string): Classification {
  let candidate: unknown;

  try {
    candidate = JSON.parse(raw);
  } catch {
    throw new Error("Model output is not valid JSON");
  }

  const parsed = ClassificationSchema.safeParse(candidate);

  if (!parsed.success) {
    throw new Error(z.prettifyError(parsed.error));
  }

  return parsed.data;
}


The important property is provenance. Classification comes from parsed.data after runtime validation, not from a cast. Validation errors can also remain structured telemetry because Zod exposes issue codes, paths, and messages identifying contract violations. Zod additionally provides z.prettifyError() when a human-readable representation is needed. 

Structured Output Reduces Syntax Risk, Not Trust Risk

Modern LLM APIs can constrain generation against JSON Schema. OpenAI Structured Outputs, for example, is documented as enforcing supplied JSON Schema rather than merely producing syntactically valid JSON, and the current API distinguishes structured output from older JSON mode. That substantially reduces malformed payloads and schema-shape errors. It does not remove the need for an application-side trust boundary, because structured responses can still be interrupted, refused, or semantically wrong even when their shape is valid. OpenAI explicitly documents incomplete responses, refusal handling, and the possibility of mistakes inside structured outputs. 

Zod 4 can convert schemas directly to JSON Schema with z.toJSONSchema(), making it possible to drive model-side constrained generation and application-side validation from the same source definition. The conversion targets JSON Schema Draft 2020-12 by default, although not every Zod feature is representable as JSON Schema; transforms, Date, Map, Set, and several other constructs require different handling. 

That limitation favors separating wire contracts from richer domain representations. The model-facing schema can remain JSON-native, while post-validation code converts ISO strings into Date objects, resolves identifiers, or calculates derived fields. Zod distinguishes schema input and output types and documents that some transformations cannot be soundly represented in JSON Schema. 

TypeScript
 
const responseSchema = z.toJSONSchema(ClassificationSchema);

const response = await client.responses.create({
  model: modelName,
  input: prompt,
  text: {
    format: {
      type: "json_schema",
      name: "classification",
      strict: true,
      schema: responseSchema,
    },
  },
});


Schema-constrained decoding and runtime validation solve different problems. Provider-side constraints narrow generation; the local parser verifies what reached the application boundary. Both layers remain useful when responses are cached or replayed, multiple providers feed the same pipeline, or tests bypass generation. JSON Schema defines structure and constraints, while validation still requires a validator where data is consumed. 

Model Business Invariants Explicitly

Structural validity is necessary but insufficient. A payload can satisfy field types while violating domain rules. A confidence value within range does not prove that the classification is correct, a valid identifier does not prove that the referenced record exists, and a syntactically valid tool argument does not prove that an action is authorized. Structured Outputs documentation similarly notes that schema-conforming responses can still contain mistakes. Runtime schemas should therefore encode deterministic invariants while leaving truth, authorization, and external-state checks to domain services. 

Cross-field rules belong in the executable contract when they are deterministic. A routing decision, for example, may require an escalation reason whenever the model chooses an escalation action. Zod refinements make such conditions enforceable without widening downstream code with repeated checks. 

TypeScript
 
const DecisionSchema = z.strictObject({
  action: z.enum(["answer", "escalate"]),
  answer: z.string().optional(),
  reason: z.string().optional(),
}).refine(
  value => value.action !== "escalate" || Boolean(value.reason),
  { error: "Escalation requires a reason" }
);


This keeps deterministic validation close to the contract without implying that a schema can establish facts outside the payload. Database existence, permissions, rate limits, and transactional constraints remain separate runtime responsibilities. JSON Schema is defined around the structure and constraints of a JSON instance, making it a format contract rather than external-state verification. 

Fail Closed and Treat Validation as a Signal

A production pipeline should not blindly coerce invalid output into the expected type. Silent defaults can turn model failures into plausible data. Invalid output is better treated as a controlled failure with bounded retry, explicit refusal and incomplete-response branches, and validation telemetry. Zod provides machine-readable issues, while structured-output APIs expose interruption and refusal states before domain execution. 

A model contract also benefits from explicit versioning. Schema changes such as renamed enum values, newly required fields, or tighter bounds can invalidate cached outputs and replayed events even when current generation is correct. Recording a schema identifier or application contract version beside generated data makes compatibility decisions explicit and prevents historical payloads from being interpreted under a newer contract. JSON Schema supports identifiers and dialect declarations for machine-readable schema metadata. 

The central engineering rule is simple: TypeScript types describe what trusted code may assume, not what an LLM actually produced. Untrusted AI output should enter the system as unknown, cross an executable runtime schema, and become a domain type only after successful validation. Provider-side structured output can reduce formatting failures, but it cannot replace local validation or domain checks. A pipeline built around that boundary preserves TypeScript’s strongest benefit without confusing compile-time confidence for runtime truth, and it converts probabilistic model output into data that deterministic application code can safely reason about.

JSON TypeScript large language model

Opinions expressed by DZone contributors are their own.

Related

  • Designing a Production-Grade Multi-Agent LLM Architecture for Structured Data Extraction
  • Token-Efficient APIs for the Agentic Era
  • Fine-Tune SLMs for Free: From Google Colab to Ollama in 7 Steps
  • Talk to Your BigQuery Data Using Claude Desktop

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook