Skip to Content

typia.llm.structuredOutput โ€” schema + parse + coerce + validate, in one shot

When you ask an LLM for structured output (one specific JSON shape, no function selection involved), you typically need four things:

  1. The schema to put in response_format / json_schema
  2. A parser that copes with messy LLM output
  3. A coercer for SDKs that already JSON-parsed
  4. A validator to confirm the result actually matches your type

typia.llm.structuredOutput<T>() returns all four bundled together for a single TypeScript type T.

signature
export namespace llm { function structuredOutput< T extends Record<string, any>, Config extends Partial<ILlmSchema.IConfig & { equals: boolean }> = {}, >(): ILlmStructuredOutput<T>; }
what-you-get
interface ILlmStructuredOutput<T> { parameters: ILlmSchema.IParameters; // hand this to the LLM parse: (input: string) => IJsonParseResult<T>; // raw string in coerce: (input: unknown) => T; // pre-parsed object in validate: (input: unknown) => IValidation<T>; // check constraints }

First example

hello-structured.ts
import typia from "typia"; interface IMember { email: string; name: string; age: number; hobbies: string[]; } const output = typia.llm.structuredOutput<IMember>(); // hand `output.parameters` to your LLM SDK as JSON schema // then: const parsed = output.parse(rawLlmText); if (parsed.success) { const v = output.validate(parsed.data); if (v.success) doSomething(v.data); }

undefined

typia
export namespace llm { export function structuredOutput< T extends Record<string, any>, Config extends Partial<ILlmSchema.IConfig & { equals: boolean }> = {}, >(): ILlmStructuredOutput<T>; }
examples/src/llm/structuredOutput.ts
import { LlmJson } from "@typia/utils"; import OpenAI from "openai"; import typia, { tags } from "typia"; interface IMember { email: string & tags.Format<"email">; name: string; age: number & tags.Minimum<0>; hobbies: string[]; joined_at: string & tags.Format<"date">; } const main = async (): Promise<void> => { // Generate structured output interface const output = typia.llm.structuredOutput<IMember>(); // Use schema with OpenAI const client: OpenAI = new OpenAI({ apiKey: "<YOUR_OPENAI_API_KEY>", }); const completion: OpenAI.ChatCompletion = await client.chat.completions.create({ model: "gpt-4o", messages: [ { role: "user", content: [ "I am a new member of the community.", "", "My name is John Doe, and I am 25 years old.", "I like playing basketball and reading books,", "and joined to this community at 2022-01-01.", ].join("\n"), }, ], response_format: { type: "json_schema", json_schema: { name: "member", schema: output.parameters as any, }, }, }); // Parse LLM response with type coercion const parsed = output.parse(completion.choices[0].message.content!); if (!parsed.success) { console.error("Parse failed:", parsed.errors); return; } // Validate the parsed data const validated = output.validate(parsed.data); if (!validated.success) { // Format errors for LLM feedback console.error(LlmJson.stringify(validated)); return; } console.log("Success:", validated.data); }; main().catch(console.error);

When to use this

If you needโ€ฆUse
One JSON shape with parse + coerce + validatestructuredOutput<T>()
One JSON shape, schema only (no helpers)parameters<T>()
Multiple functions the LLM picks betweenapplication<Class>()
Schema of an inner sub-type (advanced)schema<T>()

Parse and coerce

The same parse / coerce / validate / stringify harness as application:

raw JSON string from the LLM
const output = typia.llm.structuredOutput<IMember>(); const jsonString = '{"name": "John", "age": "25"}'; const result = output.parse(jsonString); if (result.success) { console.log(result.data.age); // 25 โ€” coerced to number }

Type coercion handles (full list in LlmJson):

  • "42" โ†’ 42 for number/integer fields
  • "true" / "false" โ†’ true / false for boolean fields
  • "null" โ†’ null for nullable fields
  • "{...}" โ†’ object (double-stringified objects)
  • "[...]" โ†’ array (double-stringified arrays)
  • Nested coercion descends into properties recursively

parse vs coerce โ€” same rule as in application.

  • Raw string from the LLM โ†’ parse(text)
  • Already-parsed object from the SDK โ†’ coerce(obj)

parse is coerce with a lenient JSON parser in front.

Validation feedback

feedback-loop.ts
import { LlmJson } from "@typia/utils"; const output = typia.llm.structuredOutput<IMember>(); const parsed = output.parse(llmResponse); if (parsed.success) { const v = output.validate(parsed.data); if (!v.success) { // Annotated JSON the LLM can read and self-correct from const feedback = LlmJson.stringify(v); // Send `feedback` back to the model in the next turn } }

The formatted output:

{ "name": "John", "age": -5, // โŒ [{"path":"$input.age","expected":"number & Minimum<0>"}] "email": "invalid", // โŒ [{"path":"$input.email","expected":"string & Format<\"email\">"}] }

Same pattern, same payoff as in application โ€” qwen3-coder-next jumps from 6.75% raw success to 100% with the harness around it.

Strict mode

const strict = typia.llm.structuredOutput<IMember, { equals: true }>(); strict.validate({ name: "John", age: 25, email: "john@example.com", hobbies: ["reading"], extraField: "not allowed", // โŒ rejected });

{ equals: true } switches the validator to validateEquals semantics. (The schema-side additionalProperties: false is always emitted on LLM parameter schemas, regardless of the flag โ€” equals only changes how the runtime validator treats stray properties at validation time.)

Restrictions

Same as parameters and schema:

  • T must be a keyworded object type with static keys
  • No dynamic keys (Record<string, V>)
  • T may not be nullable or optional
src/examples/llm.structuredOutput.violation.ts
import typia from "typia"; typia.llm.structuredOutput<string>(); typia.llm.structuredOutput<Record<string, boolean>>(); typia.llm.structuredOutput<Array<number>>();

Where to go next

  • Multiple functions, LLM picks โ†’ application
  • Just the schema, no helpers โ†’ parameters
  • Harness internals (parse, coerce, validate, stringify) โ†’ LlmJson
Last updated on