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:
- The schema to put in
response_format/json_schema - A parser that copes with messy LLM output
- A coercer for SDKs that already JSON-parsed
- 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.
export namespace llm {
function structuredOutput<
T extends Record<string, any>,
Config extends Partial<ILlmSchema.IConfig & { equals: boolean }> = {},
>(): ILlmStructuredOutput<T>;
}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
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
export namespace llm {
export function structuredOutput<
T extends Record<string, any>,
Config extends Partial<ILlmSchema.IConfig & { equals: boolean }> = {},
>(): ILlmStructuredOutput<T>;
}TypeScript Source
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 + validate | structuredOutput<T>() |
| One JSON shape, schema only (no helpers) | parameters<T>() |
| Multiple functions the LLM picks between | application<Class>() |
| Schema of an inner sub-type (advanced) | schema<T>() |
Parse and coerce
The same parse / coerce / validate / stringify harness as application:
Parsing (string โ T)
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"โ42for number/integer fields"true"/"false"โtrue/falsefor boolean fields"null"โnullfor 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
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:
Tmust be a keyworded object type with static keys- No dynamic keys (
Record<string, V>) Tmay not be nullable or optional
Source That Wonโt Compile
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