toLangChainTools() function
undefined
export function toLangChainTools(props: {
controllers: Array<ILlmController | IHttpLlmController>;
prefix?: boolean | undefined;
}): DynamicStructuredTool[];LangChain.js integration for typia.
toLangChainTools() converts TypeScript classes or OpenAPI documents into LangChain DynamicStructuredTool[] at once.
Every class method becomes a tool, JSDoc comments become tool descriptions, and TypeScript types become JSON schemas — all at compile time. For OpenAPI documents, every API endpoint is converted to a DynamicStructuredTool with schemas from the specification.
Lenient JSON parsing, type coercion, and validation feedback are all embedded automatically — the complete function calling harness that turns unreliable LLM output into 100% correct structured data.
Setup
npm install @typia/langchain @langchain/core
npm install typia
npx typia setupFrom TypeScript Class
LangChain Agent
import { ChainValues, Runnable } from "@langchain/core";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { DynamicStructuredTool } from "@langchain/core/tools";
import { ChatOpenAI } from "@langchain/openai";
import { toLangChainTools } from "@typia/langchain";
import { AgentExecutor, createToolCallingAgent } from "langchain/agents";
import typia from "typia";
import { Calculator } from "./Calculator";
const tools: DynamicStructuredTool[] = toLangChainTools({
controllers: [
typia.llm.controller<Calculator>("calculator", new Calculator()),
],
});
const agent: Runnable = createToolCallingAgent({
llm: new ChatOpenAI({ model: "gpt-4o" }),
tools,
prompt: ChatPromptTemplate.fromMessages([
["system", "You are a helpful assistant."],
["human", "{input}"],
["placeholder", "{agent_scratchpad}"],
]),
});
const executor: AgentExecutor = new AgentExecutor({ agent, tools });
const result: ChainValues = await executor.invoke({
input: "What is 10 + 5?",
});Create controllers from TypeScript classes with typia.llm.controller<Class>(), and pass them to toLangChainTools().
controllers: Array of controllers created viatypia.llm.controller<Class>()orHttpLlm.controller()prefix: Whentrue(default), tool names are formatted as{controllerName}_{methodName}. Set tofalseto use bare method names
Type Restrictions
Every method’s parameter type must be a keyworded object type with static keys — not a primitive, array, or union. The return type must also be an object type or void. Primitive return types like number or string are not allowed; wrap them in an object (e.g., { value: number }). See typia.llm.application() Restrictions for details.
From OpenAPI Document
import { DynamicStructuredTool } from "@langchain/core/tools";
import { toLangChainTools } from "@typia/langchain";
import { HttpLlm } from "@typia/utils";
const tools: DynamicStructuredTool[] = toLangChainTools({
controllers: [
HttpLlm.controller({
name: "shopping",
document: await fetch(
"https://shopping-be.wrtn.ai/editor/swagger.json",
).then((r) => r.json()),
connection: {
host: "https://shopping-be.wrtn.ai",
headers: { Authorization: "Bearer ********" },
},
}),
],
});Create controllers from OpenAPI documents with HttpLlm.controller(), and pass them to toLangChainTools().
name: Controller name used as prefix for tool namesdocument: Swagger/OpenAPI document (v2.0, v3.0, or v3.1)connection: HTTP connection info includinghostand optionalheaders
The Function Calling Harness
toLangChainTools() embeds lenient JSON parsing, type coercion, and validation feedback in every tool — all automatically. When validation fails, the error is returned as text content with inline // ❌ comments at each invalid property:
{
"name": "John",
"age": "twenty", // ❌ [{"path":"$input.age","expected":"number"}]
"email": "not-an-email", // ❌ [{"path":"$input.email","expected":"string & Format<\"email\">"}]
"hobbies": "reading" // ❌ [{"path":"$input.hobbies","expected":"Array<string>"}]
}The LLM reads this feedback and self-corrects on the next turn.
Bypassing LangChain’s Built-in Validation
LangChain internally uses @cfworker/json-schema to validate tool arguments, which throws ToolInputParsingException before custom validation can run. @typia/langchain solves this by using a passthrough Zod schema (z.record(z.unknown())), allowing typia’s much more detailed and accurate validator to handle all argument validation instead.
In the AutoBe project (AI-powered backend code generator by Wrtn Technologies ), qwen3-coder-next showed only 6.75% raw function calling success rate on compiler AST types. However, with the complete harness, it reached 100% — across all four tested Qwen models.
Working on compiler AST means working on any type and any use case.
// Compiler AST may be the hardest type structure possible
//
// Unlimited union types + unlimited depth + recursive references
export type IExpression =
| IBooleanLiteral
| INumericLiteral
| IStringLiteral
| IArrayLiteralExpression // <- recursive (contains IExpression[])
| IObjectLiteralExpression // <- recursive (contains IExpression)
| INullLiteral
| IUndefinedKeyword
| IIdentifier
| IPropertyAccessExpression // <- recursive
| IElementAccessExpression // <- recursive
| ITypeOfExpression // <- recursive
| IPrefixUnaryExpression // <- recursive
| IPostfixUnaryExpression // <- recursive
| IBinaryExpression // <- recursive (left & right)
| IArrowFunction // <- recursive (body is IExpression)
| ICallExpression // <- recursive (args are IExpression[])
| INewExpression // <- recursive
| IConditionalPredicate // <- recursive (then & else branches)
| ... // 30+ expression types totalStructured Output
Use typia.llm.parameters<T>() with LangChain’s withStructuredOutput():
import { ChatOpenAI } from "@langchain/openai";
import { dedent, LlmJson } from "@typia/utils";
import typia, { tags } from "typia";
interface IMember {
email: string & tags.Format<"email">;
name: string;
age: number & tags.Minimum<0> & tags.Maximum<100>;
hobbies: string[];
joined_at: string & tags.Format<"date">;
}
const model = new ChatOpenAI({ model: "gpt-4o" })
.withStructuredOutput(typia.llm.parameters<IMember>());
const member: IMember = await model.invoke(dedent`
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.
`);
// Validate the result
const result = typia.validate<IMember>(member);
if (!result.success) {
console.error(LlmJson.stringify(result));
}Terminal{ email: 'john.doe@example.com', name: 'John Doe', age: 25, hobbies: [ 'playing basketball', 'reading books' ], joined_at: '2022-01-01' }
The IMember interface is the single source of truth. typia.llm.parameters<IMember>() generates the JSON schema, and typia.validate<IMember>() validates the output — all from the same type. If validation fails, feed the error back to the LLM for correction.