n4nAI

TypeScript function calling: JSON Schema from types

Step-by-step guide to generating JSON Schema from TypeScript types for type-safe LLM function calling in Node.js, with runnable code and tests.

n4n Team3 min read663 words

Audio narration

Coming soon — every post will get a voice note here.

Wiring LLM tool use into a TypeScript service means you need a JSON Schema that matches your function’s input type. Hand-writing that schema duplicates your type definition and silently rots when you add a parameter. This guide shows how to derive typescript json schema function calling types directly from your TypeScript sources so the contract stays in sync and the model gets accurate tool descriptions.

Step 1: Define your function types

Start with a plain TypeScript interface for the arguments your tool accepts. Keep it free of classes, methods, and runtime-only constructs—JSON Schema can only express serializable shapes.

// tools/weather.ts
export interface GetWeatherArgs {
  latitude: number;
  longitude: number;
  units?: "metric" | "imperial";
}

export async function getWeather(args: GetWeatherArgs): Promise<{ tempC: number }> {
  // ... actual implementation
  return { tempC: 21 };
}

The GetWeatherArgs type is the single source of truth. You will compile it to JSON Schema and reuse the same type to validate the model’s invocation.

Step 2: Install a type-to-schema compiler

The typescript-json-schema package walks the TypeScript compiler API and emits a JSON Schema draft-07 document. It handles unions, optionals, and nested interfaces.

npm install --save-dev typescript-json-schema
npm install ajv # for later validation

If you prefer a zero-runtime approach, typia or ts-to-json-schema work similarly. The examples below use typescript-json-schema because it maps cleanly to OpenAI’s function-calling format.

Step 3: Generate the schema at build time

Write a small Node script that loads your TS program and prints the schema for a named type. Run it in your prebuild or a dedicated gen:schema npm script.

// scripts/gen-schema.ts
import * as path from "path";
import { generateSchema } from "typescript-json-schema";

const filePath = path.resolve("tools/weather.ts");
const schema = generateSchema(
  {
    path: filePath,
  },
  "GetWeatherArgs",
  {
    required: true,
    ref: false,
    noExtraProps: true,
  }
);

if (!schema) {
  throw new Error("Failed to generate schema");
}

process.stdout.write(JSON.stringify(schema, null, 2));

Execute it:

npx ts-node scripts/gen-schema.ts > tools/weather.schema.json

The output is a self-contained schema:

{
  "type": "object",
  "properties": {
    "latitude": { "type": "number" },
    "longitude": { "type": "number" },
    "units": { "type": "string", "enum": ["metric", "imperial"] }
  },
  "required": ["latitude", "longitude"],
  "additionalProperties": false
}

Because we passed ref: false and noExtraProps: true, the schema has no $ref pointers and rejects unknown keys—both expected by most LLM providers.

Step 4: Validate the generated schema locally

Before shipping, confirm the schema accepts good input and rejects bad input. Use Ajv in a quick test.

// scripts/verify-schema.ts
import Ajv from "ajv";
import { readFileSync } from "fs";

const schema = JSON.parse(readFileSync("tools/weather.schema.json", "utf8"));
const ajv = new Ajv({ strict: true });
const validate = ajv.compile(schema);

console.assert(validate({ latitude: 52.5, longitude: 13.4 }) === true, "valid obj should pass");
console.assert(validate({ latitude: "bad" }) === false, "wrong type should fail");
console.assert(validate({ latitude: 1, longitude: 2, extra: 3 }) === false, "extra prop should fail");
console.log("schema verification ok");

Run npx ts-node scripts/verify-schema.ts. If all assertions hold, your typescript json schema function calling types are structurally sound.

Step 5: Register the tool with the model

LLM APIs expect a tools array where each entry has a function object containing name, description, and parameters (the JSON Schema). Load the generated file and attach it.

import { readFileSync } from "fs";
import OpenAI from "openai";

const weatherSchema = JSON.parse(readFileSync("tools/weather.schema.json", "utf8"));

const client = new OpenAI(); // or point baseURL at a gateway

const response = await client.chat.completions.create({
  model: "gpt-4o-mini",
  messages: [{ role: "user", content: "What's the temperature in Berlin?" }],
  tools: [
    {
      type: "function",
      function: {
        name: "getWeather",
        parameters: weatherSchema,
      },
    },
  ],
  tool_choice: "auto",
});

If you route through a gateway such as n4n.ai, the OpenAI-compatible endpoint accepts this same payload and will fall back across providers when one is rate-limited, while forwarding your schema unchanged.

The model returns a tool_calls entry with arguments as a JSON string. Those arguments must be validated against the same schema before you invoke your function.

Step 6: Validate model output at runtime

Never trust the model to emit conforming JSON. Compile the schema once and reuse the validator in your request path.

// tools/executor.ts
import Ajv from "ajv";
import { readFileSync } from "fs";
import { getWeather, GetWeatherArgs } from "./weather";

const schema = JSON.parse(readFileSync("./weather.schema.json", "utf8"));
const validate = new Ajv().compile(schema);

export async function executeToolCall(name: string, argsJson: string) {
  const args = JSON.parse(argsJson);
  if (!validate(args)) {
    throw new Error(`Invalid args for ${name}: ${JSON.stringify(validate.errors)}`);
  }
  if (name === "getWeather") {
    return getWeather(args as GetWeatherArgs);
  }
  throw new Error(`Unknown tool ${name}`);
}

Casting to GetWeatherArgs is safe only after validation passes. This closes the loop: the typescript json schema function calling types guard both the outbound contract and the inbound call.

Step 7: End-to-end verification

Write a test that simulates a model response and confirms the executor runs the real function. Use node:test or your framework of choice.

// test/executor.test.ts
import test from "node:test";
import assert from "node:assert/strict";
import { executeToolCall } from "../tools/executor";

test("executes getWeather with valid model args", async () => {
  const fakeModelArgs = JSON.stringify({ latitude: 52.52, longitude: 13.405 });
  const result = await executeToolCall("getWeather", fakeModelArgs);
  assert.ok(typeof result.tempC === "number");
});

test("rejects malformed model args", async () => {
  const badArgs = JSON.stringify({ latitude: "north" });
  await assert.rejects(() => executeToolCall("getWeather", badArgs));
});

Run node --test. Green tests mean the schema generation, validation, and function binding all work.

Keeping types and schema in sync

Add a pre-commit hook or CI step that regenerates schemas and fails if the file changes unexpectedly:

npx ts-node scripts/gen-schema.ts > tools/weather.schema.json
git diff --exit-code tools/weather.schema.json

If a developer edits GetWeatherArgs without regenerating, CI catches the drift. This is the core payoff of deriving typescript json schema function calling types from code rather than maintaining a second copy by hand.

Handling complex types

typescript-json-schema supports enums, tuples, and nested interfaces. For recursive types, set ref: true and resolve $ref with a library like json-schema-merge-allof or inline them in a post-process step. Avoid Date or Map in tool argument types—serialize them as strings or records so the schema stays JSON-compatible.

When to generate at runtime

Build-time generation is safest for production. If you need dynamic schemas (e.g., per-tenant config), run the compiler in a worker at startup. The compiler pulls in the TypeScript standard library, so cache the result; don’t invoke it per request.

Verify success

You have succeeded when:

  1. tools/weather.schema.json exists and matches your interface.
  2. scripts/verify-schema.ts prints schema verification ok.
  3. node --test passes both executor tests.
  4. A real chat completion with tools returns a tool_calls entry that executeToolCall accepts and runs.

At that point your function-calling layer is type-safe from the TS definition to the validated model invocation. The typescript json schema function calling types are now a single artifact, not three.

Tagstypescriptjson-schemafunction-callingtype-generation

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All function calling in typescript/node.js posts →