ProvenPin Install

Zod v4 schemas exported to JSON Schema and checked with Ajv: working versions (tested 2026-10-15)

Export Zod v4 schemas to JSON Schema 2020-12 with z.toJSONSchema, using .meta() ids and descriptions and input vs output modes, then validate the same data with Ajv 2020.

passed4 passed · 0 failed2026-10-15 17:11 UTC
Status
passed
Last passed
2026-10-15 17:11 UTC
Tests
4 / 4
Language
TypeScript
Runtime
node v22.23.3
pinned versions4
zod@4.6.7ajv@8.20.0@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38775053802

The code

Exactly the files that passed. Your agent can pull them with get_recipe("zod-v4-json-schema-export") (install), or see them on GitHub.

package.json
{
  "name": "zod-v4-json-schema-export",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "zod": "4.6.7",
    "ajv": "8.20.0"
  },
  "devDependencies": {
    "@types/node": "26.6.5",
    "tsx": "4.23.15"
  }
}
src/schemas.ts
import { z } from "zod";

export const Address = z
  .object({ street: z.string().min(1), city: z.string(), zip: z.string().regex(/^\d{5}$/) })
  .meta({ id: "Address", description: "Postal address" });

export const Customer = z.object({
  id: z.uuid(),
  email: z.email(),
  name: z.string().min(1).max(80),
  tier: z.enum(["free", "pro"]).default("free"),
  tags: z.array(z.string()).max(5).optional(),
  address: Address,
});

export type Customer = z.infer<typeof Customer>;

/** JSON Schema of what a client may send (defaults make fields optional). */
export function customerInputSchema() {
  return z.toJSONSchema(Customer, { target: "draft-2020-12", io: "input" });
}

/** JSON Schema of what the API returns after parsing (defaults applied, so required). */
export function customerOutputSchema() {
  return z.toJSONSchema(Customer, { target: "draft-2020-12", io: "output" });
}
test/schemas.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import Ajv2020 from "ajv/dist/2020.js";
import { Customer, customerInputSchema, customerOutputSchema } from "../src/schemas.ts";

const sample = {
  id: "5f0c6c1e-3b7a-4c1e-9a52-1d2f3e4a5b6c",
  email: "[email protected]",
  name: "Ada",
  address: { street: "1 Main St", city: "Kenai", zip: "99611" },
};

test("exports draft 2020-12 with metadata and shared $defs", () => {
  const s = customerOutputSchema() as any;
  assert.equal(s.$schema, "https://json-schema.org/draft/2020-12/schema");
  assert.equal(s.properties.address.$ref, "#/$defs/Address");
  assert.equal(s.$defs.Address.description, "Postal address");
  assert.equal(s.properties.email.format, "email");
  assert.equal(s.properties.id.format, "uuid");
  assert.equal(s.properties.name.maxLength, 80);
  assert.deepEqual(s.properties.tier.enum, ["free", "pro"]);
});

test("input vs output: defaulted fields are optional on input only", () => {
  const input = customerInputSchema() as any;
  const output = customerOutputSchema() as any;
  assert.equal(input.required.includes("tier"), false);
  assert.equal(output.required.includes("tier"), true);
  assert.equal(output.required.includes("tags"), false);
});

test("Ajv and Zod agree on the same data", () => {
  const ajv = new Ajv2020({ allErrors: true, strict: false });
  const validate = ajv.compile(customerInputSchema());
  assert.equal(validate(sample), true);
  assert.equal(Customer.safeParse(sample).success, true);

  const bad = { ...sample, address: { ...sample.address, zip: "9961" }, extra: 1 };
  assert.equal(validate(bad), false);
  assert.ok(validate.errors!.some((e) => e.instancePath === "/address/zip" && e.keyword === "pattern"));
  assert.equal(Customer.safeParse(bad).success, false);
});

test("parsing applies defaults that the output schema requires", () => {
  const parsed = Customer.parse(sample);
  assert.equal(parsed.tier, "free");
  const ajv = new Ajv2020({ strict: false });
  assert.equal(ajv.validate(customerOutputSchema(), parsed), true);
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.