ProvenPin Install

Ajv standalone validation code generated at build time: working versions (tested 2026-10-15)

Compile JSON Schemas once with Ajv into standalone ESM validator modules, write them to disk and import them at runtime without compiling, checking errors, defaults and type coercion.

passed4 passed · 0 failed2026-10-15 17:10 UTC
Status
passed
Last passed
2026-10-15 17:10 UTC
Tests
4 / 4
Language
TypeScript
Runtime
node v22.23.3
pinned versions3
ajv@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("ajv-standalone-validation") (install), or see them on GitHub.

package.json
{
  "name": "ajv-standalone-validation",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "ajv": "8.20.0"
  },
  "devDependencies": {
    "@types/node": "26.6.5",
    "tsx": "4.23.15"
  }
}
src/build.ts
import fs from "node:fs";
import path from "node:path";
import Ajv from "ajv";
import standaloneCode from "ajv/dist/standalone/index.js";

export const userSchema = {
  $id: "user",
  type: "object",
  required: ["email", "age"],
  additionalProperties: false,
  properties: {
    email: { type: "string", pattern: "^[^@\\s]+@[^@\\s]+$" },
    age: { type: "integer", minimum: 0 },
    plan: { type: "string", enum: ["free", "pro"], default: "free" },
  },
} as const;

export const querySchema = {
  $id: "query",
  type: "object",
  properties: { page: { type: "integer", minimum: 1, default: 1 }, q: { type: "string" } },
} as const;

/** Generates one ESM module exporting a validator per schema $id. */
export function buildValidators(outFile: string): string {
  const ajv = new Ajv({
    schemas: [userSchema, querySchema],
    allErrors: true,
    useDefaults: true,
    coerceTypes: true,
    code: { source: true, esm: true },
  });
  const code = standaloneCode(ajv, { validateUser: "user", validateQuery: "query" });
  fs.mkdirSync(path.dirname(outFile), { recursive: true });
  fs.writeFileSync(outFile, code);
  return code;
}
test/build.test.ts
import { test, before } from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { buildValidators } from "../src/build.ts";

type Validator = ((data: unknown) => boolean) & { errors?: Array<{ instancePath: string; keyword: string }> | null };
let validateUser: Validator;
let validateQuery: Validator;
let code = "";

before(async () => {
  // Written inside the project so the generated module can resolve ajv runtime helpers if it needs them.
  const out = path.resolve("generated/validators.mjs");
  code = buildValidators(out);
  const mod = await import(pathToFileURL(out).href);
  validateUser = mod.validateUser;
  validateQuery = mod.validateQuery;
});

test("generated module is plain ESM without a schema compiler", () => {
  assert.match(code, /export const validateUser/);
  assert.match(code, /export const validateQuery/);
  assert.doesNotMatch(code, /new Ajv/);
});

test("valid data passes and gets defaults", () => {
  const user: Record<string, unknown> = { email: "[email protected]", age: 36 };
  assert.equal(validateUser(user), true);
  assert.equal(user.plan, "free");
});

test("allErrors reports every problem", () => {
  assert.equal(validateUser({ email: "nope", age: -1, extra: true }), false);
  const keywords = validateUser.errors!.map((e) => e.keyword).sort();
  assert.deepEqual(keywords, ["additionalProperties", "minimum", "pattern"]);
});

test("coerces query string values", () => {
  const query: Record<string, unknown> = { page: "3" };
  assert.equal(validateQuery(query), true);
  assert.equal(query.page, 3);
  const empty: Record<string, unknown> = {};
  assert.equal(validateQuery(empty), true);
  assert.equal(empty.page, 1);
});

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.