zod-to-json-schema
This package walks a Zod 3 schema and emits the equivalent JSON Schema document, so a validator you already wrote in TypeScript can also describe an OpenAPI request body, an LLM tool definition, or a form config. It handles nested objects, unions, recursion through internal $refs, string and number constraints, and can target draft-07, 2019-09, OpenAPI 3.0, or OpenAI structured output mode. It matters far beyond its 1.2k stars because it sits underneath LangChain, MCP servers, and other agent tooling that needs a JSON Schema for every tool. Read the next section before adding it: the author deprecated it in November 2025 and the repository is archived.
Do not add this to a new project: Zod 4 does the same job in core and this repository is archived. It stays worth knowing because millions of installs a week arrive through agent and OpenAPI tooling that has not migrated yet, and configuring it correctly is often the difference between a tool schema an LLM provider accepts and one it rejects.
Use it if
- You are stuck on Zod 3 schemas, in your own code or through a dependency, and still need JSON Schema out of them; this is the only mature converter for that shape
- You already depend on it transitively through LangChain, an MCP server SDK, or a similar package and want to understand and configure what it produces
- You need OpenAPI 3.0 output specifically, where nullable and the missing 3.1 alignment mean plain draft-07 will not do
- You need output shaped for OpenAI structured outputs, where optional properties have to become required and nullable
- You are on Zod 4: it ships z.toJSONSchema() in core, maintained by the Zod author, and the deprecation notice in this README points you there directly. Adding this package to a Zod 4 project means one more dependency for something you already have
- You need this maintained. The repository is archived, the last release was 3.25.2 in March 2026, and there is nowhere to file a bug. New JSON Schema drafts, new Zod features, and any newly discovered conversion bug will stay unfixed
- You want predictable versioning: the README states outright that the package does not follow semantic versioning. Major and minor track Zod feature parity, and API additions have shipped as patch releases
- Your schemas lean on .transform() or .preprocess(): the output side of a transform has no schema to convert, so you get the input shape back, and the object parser calls .isOptional() which can invoke your preprocess callbacks with undefined during conversion
- You want the schema to be the source of truth rather than the TypeScript types. Writing JSON Schema first with something like TypeBox avoids the conversion layer and its edge cases entirely
Setup reality
npm install zod-to-json-schema, import zodToJsonSchema, call it. There is no config file and zod is a peer dependency accepting ^3.25.28 or ^4, so npm and pnpm will not add it for you. The trap is that peer range: version 3.25 accepts Zod 4 as a peer but does not accept Zod 4 schemas, so on a Zod 4 project you have to import { z } from 'zod/v3' and keep writing v3 schemas for anything you pass in. Handing it a v4 schema fails at runtime rather than at compile time. Beyond that, the defaults surprise people: output is draft-07, a name string as the second argument wraps everything in a definitions block behind a $ref, and objects get additionalProperties: false by default, which some consumers (Google's Gen AI API, for one) reject outright until you set allowedAdditionalProperties and rejectedAdditionalProperties to undefined.
Patterns
Convert a schema to JSON Schemabasic-convert
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
const User = z.object({
name: z.string().min(2),
age: z.number().int().optional(),
});
const schema = zodToJsonSchema(User);
// { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', ... }Default target is draft-07 and objects come out with additionalProperties: false, mirroring how Zod strips unknown keys when parsing.
Name the schema and put it in definitionsnamed-definitions
const schema = zodToJsonSchema(User, 'User');
// { $ref: '#/definitions/User', definitions: { User: { ... } } }
// or with the options object
const schema2 = zodToJsonSchema(User, {
name: 'User',
definitionPath: '$defs',
});Passing a bare string as the second argument is the same as { name }. Consumers that expect a plain object at the root will choke on the $ref wrapper, so only name it when the caller understands definitions.
Target OpenAPI 3.0openapi-3-target
const schema = zodToJsonSchema(User, {
target: 'openApi3',
$refStrategy: 'none',
});OpenAPI 3.0 predates JSON Schema draft-07 compatibility, so nullable and a few keywords differ. OpenAPI 3.1 is regular JSON Schema, so leave the default target there.
Produce a schema for OpenAI structured outputopenai-tool-schema
const Args = z.object({
city: z.string(),
units: z.enum(['c', 'f']).optional(),
});
const parameters = zodToJsonSchema(Args, {
target: 'openAi',
$refStrategy: 'none',
});
const tool = {
type: 'function',
function: { name: 'get_weather', parameters },
};The openAi target rewrites optional properties as required-but-nullable, which is what strict mode demands. The README calls this target experimental, so some option combinations still produce schemas the API rejects.
Hoist repeated sub-schemasshared-definitions
const Address = z.object({ street: z.string(), zip: z.string() });
const Order = z.object({ billing: Address, shipping: Address });
const schema = zodToJsonSchema(Order, {
definitions: { Address },
});
// billing and shipping both become { $ref: '#/definitions/Address' }Without this, the same object literal is inlined at every use site. It works alongside a schema name and a custom definitionPath.
Inline everything instead of using $refno-refs
const schema = zodToJsonSchema(Order, { $refStrategy: 'none' });Many LLM providers and form builders do not resolve $ref. 'none' inlines repeated schemas, but a genuinely recursive schema degrades to {} because it cannot be expanded, so check recursive types after switching.
Drop the additionalProperties keyword entirelyadditional-properties
const schema = zodToJsonSchema(User, {
allowedAdditionalProperties: undefined,
rejectedAdditionalProperties: undefined,
});Some APIs reject the keyword outright rather than reading its value. Both options are ignored if your object uses .catchall(), because the catchall schema is emitted instead.
Carry Zod error messages into the outputerror-messages
const Email = z.string().email('Invalid email').min(5, 'Too short');
const schema = zodToJsonSchema(Email, { errorMessages: true });
// { type: 'string', format: 'email', minLength: 5,
// errorMessage: { format: 'Invalid email', minLength: 'Too short' } }The errorMessage keyword is an ajv-errors extension, not standard JSON Schema. Plain ajv ignores it until you install and enable ajv-errors.
Attach title and examples through the descriptionadd-metadata
import { zodToJsonSchema, jsonDescription } from 'zod-to-json-schema';
const schema = zodToJsonSchema(
z.string().describe(
JSON.stringify({ title: 'City', examples: ['Ahmedabad', 'Pune'] }),
),
{ postProcess: jsonDescription },
);Zod 3 has no metadata API, so this JSON-in-the-description trick is the supported workaround. Zod 4's .meta() removes the need for it, which is one more reason the package was retired.
Override or remove a specific propertyoverride-output
import { zodToJsonSchema, ignoreOverride } from 'zod-to-json-schema';
const schema = zodToJsonSchema(User, {
override: (def, refs) => {
if (refs.currentPath.join('/') === '#/properties/age') {
return { type: 'integer', minimum: 0 };
}
return ignoreOverride;
},
});You must return the ignoreOverride symbol to leave a node alone. Returning undefined deletes the property from the output, which is a very easy accident to ship.
Keep using it on a Zod 4 projectzod-v3-under-zod-4
import { z } from 'zod/v3';
import { zodToJsonSchema } from 'zod-to-json-schema';
const Legacy = z.object({ id: z.string().uuid() });
const schema = zodToJsonSchema(Legacy);Version 3.25 accepts Zod 4 as a peer dependency but only converts v3 schemas. Passing a schema built from the v4 import fails at runtime, not at type-check time.
Replace it with the Zod 4 built-inmigrate-to-zod-4
import { z } from 'zod';
const User = z.object({
name: z.string().min(2),
age: z.number().int().optional(),
});
const schema = z.toJSONSchema(User, { target: 'draft-7' });Output is not byte-identical to this package: default target, $ref placement, and how unrepresentable types are handled all differ, so diff a real schema against your consumer before swapping.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| zod | npm | You are on or can move to Zod 4, where z.toJSONSchema() replaces this package and is maintained alongside the validator |
| @sinclair/typebox | npm | You would rather write JSON Schema directly and derive TypeScript types from it, so no conversion step exists to go wrong |
| @valibot/to-json-schema | npm | You are picking a validator now and want a smaller, tree-shakeable one with an officially maintained JSON Schema converter |