npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@graftjs/zod

v0.7.0

Published

Type-safe bidirectional schema mapping - Zod adapter

Readme

@graftjs/zod

Type-safe bidirectional schema mapping for Zod

High-level Zod integration for Graft. Map between two Zod schemas with automatic field detection, runtime validation, and compile-time type safety.

Why Graft?

When building anti-corruption layers or integrating with external APIs, you need to map between different schemas (e.g., your database models ↔ API responses). Graft makes this:

  • Type-safe: Compile-time errors if you wire incompatible types
  • Bidirectional: One config gives you both directions
  • Fast: Precompiled mapping specs for hot paths
  • Safe: Built-in prototype pollution prevention
  • Ergonomic: Auto-mapping reduces boilerplate

Installation

npm install @graftjs/zod zod
# or
pnpm add @graftjs/zod zod
# or
yarn add @graftjs/zod zod

Requirements:

  • Node.js >= 18
  • TypeScript >= 5.0
  • Zod >= 4.0

Quick Start

import { z } from 'zod';
import { createGraft } from '@graftjs/zod';

// Define your schemas
const UserDB = z.object({
  user_id: z.string(),
  email_address: z.string(),
  created_at: z.string()
});

const UserAPI = z.object({
  id: z.string(),
  email: z.string(),
  createdAt: z.string()
});

// Create a graft with callback syntax (full autocomplete!)
const userGraft = createGraft(UserDB, UserAPI, fields => ({
  id: fields.user_id,        // Full autocomplete on `fields.`
  email: fields.email_address,
  createdAt: fields.created_at
})).named('db', 'api');

// Convert DB → API
const apiUser = userGraft.toApi({
  user_id: '123',
  email_address: '[email protected]',
  created_at: '2024-01-01T00:00:00Z'
});
// { id: '123', email: '[email protected]', createdAt: '2024-01-01T00:00:00Z' }

// Convert API → DB
const dbUser = userGraft.toDb({
  id: '123',
  email: '[email protected]',
  createdAt: '2024-01-01T00:00:00Z'
});
// { user_id: '123', email_address: '[email protected]', created_at: '2024-01-01T00:00:00Z' }

Core Concepts

Auto-Mapping

Fields with matching names and compatible types are automatically mapped:

const UserDB = z.object({
  id: z.string(),
  email: z.string(),
  age: z.number()
});

const UserAPI = z.object({
  id: z.string(),       // ✅ Auto-mapped (same name, compatible type)
  email: z.string(),    // ✅ Auto-mapped
  age: z.number(),      // ✅ Auto-mapped
  name: z.string()      // ❌ Not in UserDB - must map manually
});

// No config needed for id, email, age!
const graft = createGraft(UserDB, UserAPI, fields => ({
  name: fields.username  // Only map what's different
}));

Auto-mapping rules:

  • Exact key name match required
  • Types must be compatible (after unwrapping optional/nullable)
  • Conservative: different types never auto-map

Bidirectional Conversion

One configuration automatically gives you both directions:

const graft = createGraft(UserDB, UserAPI, fields => ({
  id: fields.user_id
})).named('db', 'api');

// Forward: Left → Right (DB → API)
const apiUser = graft.toApi(dbUser);

// Backward: Right → Left (API → DB)
const dbUser = graft.toDb(apiUser);

// Round-trip safety
assert.deepEqual(dbUser, graft.toDb(graft.toApi(dbUser)));

Without .named() - use generic toRight/toLeft:

const graft = createGraft(UserDB, UserAPI, fields => ({
  id: fields.user_id
}));

graft.toRight(dbUser);  // Left → Right
graft.toLeft(apiUser);  // Right → Left

Validation Modes

Control when validation occurs:

const graft = createGraft(UserDB, UserAPI, {}, {
  validate: 'both'  // Default: validate input AND output
});

// Other modes:
validate: 'from'   // Only validate source schema
validate: 'to'     // Only validate target schema
validate: 'none'   // Skip all validation (fastest, least safe)

Strict Mode

Ensure all required fields are mapped:

// ❌ Type error at compile time!
const graft = createGraft(UserDB, UserAPI, {
  // Missing mapping for required field 'name'
});
// Type: { __error: "Missing required mappings"; __missing: "name" }

// ✅ Valid
const graft = createGraft(UserDB, UserAPI, {
  name: 'username'  // All required fields covered
});

Strict modes:

  • 'to' (default): All required target fields must be mapped
  • 'from': All required source fields must be reproducible
  • 'both': Both directions enforced
  • 'none': No enforcement (permissive)

Nested Grafts

Compose smaller grafts for nested object mapping:

// Create a graft for addresses
const AddressDB = z.object({
  street_name: z.string(),
  city_name: z.string(),
  zip_code: z.string()
});

const AddressAPI = z.object({
  street: z.string(),
  city: z.string(),
  zip: z.string()
});

const addressGraft = createGraft(AddressDB, AddressAPI, fields => ({
  street: fields.street_name,
  city: fields.city_name,
  zip: fields.zip_code
}));

// Use the address graft in a parent graft
const UserDB = z.object({
  user_id: z.string(),
  user_name: z.string(),
  address: AddressDB
});

const UserAPI = z.object({
  id: z.string(),
  name: z.string(),
  address: AddressAPI
});

const userGraft = createGraft(UserDB, UserAPI, fields => ({
  id: fields.user_id,
  name: fields.user_name,
  address: {
    from: 'address',      // source path
    graft: addressGraft   // nested graft to apply
  }
})).named('db', 'api');

// Both directions work automatically!
const apiUser = userGraft.toApi(dbUser);  // Address transformed via addressGraft
const dbUser = userGraft.toDb(apiUser);   // Inverse also works

Nested graft features:

  • Deep source paths: { from: 'user.data.address', graft: addressGraft }
  • Optional nested objects handled correctly (undefined skipped, null passed)
  • Deeply nested grafts (3+ levels) supported
  • Works alongside transforms and auto-mapping
  • Clear error context via GraftNestedError

Transforms

Apply functions to values during mapping:

import { dateString, centsToPrice, pipe, map } from '@graftjs/zod';

const graft = createGraft(ProductDB, ProductAPI, {
  id: 'product_id',
  createdAt: {
    from: 'created_at',
    ...dateString()  // ISO string ↔ Date
  },
  price: {
    from: 'price_cents',
    ...centsToPrice()  // cents ↔ dollars
  }
});

Built-in transforms: dateString, timestamp, timestampMs, centsToPrice, csvToArray, jsonString, base64, uppercase, lowercase, trim, boolToInt

API Reference

createGraft(leftSchema, rightSchema, configOrCallback?, options?)

Create a bidirectional schema mapper.

Type Signature:

function createGraft<LeftSchema, RightSchema, Config>(
  leftSchema: z.ZodObject<any>,
  rightSchema: z.ZodObject<any>,
  configOrCallback?: Config | ((fields: KeyProxy<z.infer<LeftSchema>>) => Config),
  options?: GraftOptions
): Graft<z.infer<LeftSchema>, z.infer<RightSchema>>

Parameters:

  • leftSchema: Left (source) Zod object schema
  • rightSchema: Right (target) Zod object schema
  • configOrCallback: Mapping configuration - object or callback (optional if fully auto-mappable)
  • options: Graft options (optional)

Returns: Graft<Left, Right> - Frozen graft instance

Example:

// Callback syntax (recommended - full autocomplete!)
const userGraft = createGraft(
  UserDB,
  UserAPI,
  fields => ({ id: fields.user_id, email: fields.email_address }),
  { strict: 'to', validate: 'both' }
).named('db', 'api');

// Object syntax (backwards compatible)
const userGraft = createGraft(
  UserDB,
  UserAPI,
  { id: 'user_id', email: 'email_address' }
);

defineGraft(fromSchema, toSchema)

Curried version for better type inference.

const defineUserGraft = defineGraft(UserDB, UserAPI);

// Better type inference for complex configs
const userGraft = defineUserGraft({
  id: 'user_id',
  email: 'email_address'
}, {
  strict: 'both'
});

Graft Instance Methods

toRight(input: unknown): Right

Convert from left to right schema (Left → Right).

Process:

  1. Parse and validate input with leftSchema
  2. Execute mapping
  3. Parse and validate output with rightSchema

Throws: GraftValidationError if validation fails

const apiUser = userGraft.toRight({
  user_id: '123',
  email_address: '[email protected]'
});

toLeft(input: unknown): Left

Convert from right to left schema (Right → Left, inverse).

const dbUser = userGraft.toLeft({
  id: '123',
  email: '[email protected]'
});

mapRight(input: Left): Right

Fast path - skip input validation.

Use when you already have a validated Left object:

// Assuming dbUser is already validated
const apiUser = userGraft.mapRight(dbUser);

Performance benefit: Skips source schema parsing

mapLeft(input: Right): Left

Fast path - skip input validation.

// Assuming apiUser is already validated
const dbUser = userGraft.mapLeft(apiUser);

.named(leftName, rightName) or .named({ left, right })

Create custom-named method variants:

const graft = createGraft(UserDB, UserAPI, fields => ({
  id: fields.user_id
})).named('db', 'api');

// Now you have semantic methods:
graft.toApi(dbUser);   // Left → Right
graft.toDb(apiUser);   // Right → Left
graft.mapApi(dbUser);  // Fast path Left → Right
graft.mapDb(apiUser);  // Fast path Right → Left

spec: CompiledSpec

Access the compiled mapping specification (for debugging).

console.log(userGraft.spec.forward);
// [{ from: 'user_id', to: 'id' }, ...]

Examples

Example 1: Database ↔ API

const ProductDB = z.object({
  product_id: z.string(),
  product_name: z.string(),
  price_cents: z.number(),
  created_at: z.string()
});

const ProductAPI = z.object({
  id: z.string(),
  name: z.string(),
  price: z.number(),
  createdAt: z.string()
});

const productGraft = createGraft(ProductDB, ProductAPI, fields => ({
  id: fields.product_id,
  name: fields.product_name,
  price: fields.price_cents,
  createdAt: fields.created_at
})).named('db', 'api');

// Use in route handler
app.get('/products/:id', async (req, res) => {
  const dbProduct = await db.products.findOne(req.params.id);
  const apiProduct = productGraft.toApi(dbProduct);
  res.json(apiProduct);
});

app.post('/products', async (req, res) => {
  const dbProduct = productGraft.toDb(req.body);
  await db.products.create(dbProduct);
  res.json({ success: true });
});

Example 2: Third-Party Integration

const StripeCustomer = z.object({
  id: z.string(),
  email: z.string(),
  name: z.string().nullable(),
  metadata: z.record(z.string())
});

const InternalUser = z.object({
  stripeId: z.string(),
  email: z.string(),
  fullName: z.string().optional(),
  metadata: z.record(z.string())
});

const stripeGraft = createGraft(StripeCustomer, InternalUser, fields => ({
  stripeId: fields.id,
  fullName: fields.name
  // email and metadata auto-mapped (same names)
})).named('stripe', 'internal');

// Sync from Stripe webhook
async function handleStripeWebhook(event: any) {
  const stripeCustomer = event.data.object;
  const internalUser = stripeGraft.toInternal(stripeCustomer);
  await saveUser(internalUser);
}

Example 3: Mixed Auto + Manual Mapping

const UserDB = z.object({
  user_id: z.string(),
  email: z.string(),      // Same name
  age: z.number(),        // Same name
  created_at: z.string()
});

const UserAPI = z.object({
  id: z.string(),
  email: z.string(),      // Auto-mapped
  age: z.number(),        // Auto-mapped
  createdAt: z.string()
});

// Only map what's different - email and age auto-mapped!
const graft = createGraft(UserDB, UserAPI, fields => ({
  id: fields.user_id,
  createdAt: fields.created_at
})).named('db', 'api');

Example 4: Optional Fields

const UserDB = z.object({
  user_id: z.string(),
  middle_name: z.string().optional(),
  nickname: z.string().nullable()
});

const UserAPI = z.object({
  id: z.string(),
  middleName: z.string().optional(),
  nickname: z.string().nullable()
});

const graft = createGraft(UserDB, UserAPI, fields => ({
  id: fields.user_id,
  middleName: fields.middle_name
  // nickname auto-mapped (same name)
}));

// Optional fields handled correctly
const user = graft.toRight({ user_id: '123' });
// { id: '123' } - optional fields omitted when undefined

Error Handling

Validation Errors

import { GraftValidationError } from '@graftjs/zod';

try {
  const apiUser = userGraft.toRight(invalidData);
} catch (error) {
  if (error instanceof GraftValidationError) {
    console.error(`Validation failed during ${error.direction} conversion`);
    console.error(error.zodError.issues); // Original Zod errors
  }
}

Configuration Errors

import { GraftCollisionError, GraftSecurityError } from '@graftjs/zod';

try {
  const graft = createGraft(UserDB, UserAPI, {
    id: 'user_id',
    id: 'other_field'  // ❌ Collision!
  });
} catch (error) {
  if (error instanceof GraftCollisionError) {
    console.error(`Collision on target: ${error.targetKey}`);
    console.error(`Conflicting sources: ${error.sourceKeys.join(', ')}`);
  }
}

try {
  const graft = createGraft(UserDB, UserAPI, {
    '__proto__': 'evil'  // ❌ Security violation!
  });
} catch (error) {
  if (error instanceof GraftSecurityError) {
    console.error(`Dangerous key blocked: ${error.dangerousKey}`);
  }
}

Best Practices

1. Create Grafts Once

// ✅ Good - create once, reuse many times
export const userGraft = createGraft(UserDB, UserAPI, fields => ({
  id: fields.user_id
})).named('db', 'api');

// Use in multiple places
app.get('/users/:id', async (req, res) => {
  const dbUser = await getUser(req.params.id);
  res.json(userGraft.toApi(dbUser));
});

// ❌ Bad - recreating on every request
app.get('/users/:id', async (req, res) => {
  const graft = createGraft(UserDB, UserAPI, fields => ({ id: fields.user_id }));
  res.json(graft.toRight(dbUser)); // Wasteful!
});

2. Use Type Inference

// ✅ Good - let TypeScript infer types
const userGraft = createGraft(UserDB, UserAPI, fields => ({ id: fields.user_id }));
type ApiUser = ReturnType<typeof userGraft.toRight>;

// ❌ Less good - manually typing everything
const userGraft: Graft<DbUser, ApiUser> = createGraft(...);

3. Validate at Boundaries

// ✅ Good - validate external data
const apiUser = userGraft.toRight(untrustedInput);

// ✅ Also good - skip validation for internal data
const apiUser = userGraft.mapRight(alreadyValidatedDbUser);

4. Explicit is Better Than Implicit

// ✅ Good - explicit mappings are self-documenting
const graft = createGraft(UserDB, UserAPI, fields => ({
  id: fields.user_id,
  email: fields.email_address,
  createdAt: fields.created_at
}));

// ⚠️  Less clear - relies on auto-mapping
const graft = createGraft(UserDB, UserAPI);

Type Safety Features

Compile-Time Type Checking

const UserDB = z.object({
  user_id: z.string(),
  age: z.number()
});

const UserAPI = z.object({
  id: z.string(),
  age: z.string()  // Different type!
});

// ❌ Type error - age is number in DB but string in API
const graft = createGraft(UserDB, UserAPI, {
  id: 'user_id',
  age: 'age'  // Type incompatibility caught at compile time!
});

Required Field Coverage

const UserAPI = z.object({
  id: z.string(),
  email: z.string(),
  name: z.string()  // Required field
});

// ❌ Type error - missing mapping for 'name'
const graft = createGraft(UserDB, UserAPI, {
  id: 'user_id',
  email: 'email_address'
  // Where's name?
});

// Hover over the error to see:
// Type: { __error: "Missing required mappings"; __missing: "name" }

Performance Optimization

Graft is designed for performance:

  1. Precompiled Specs: Mapping logic compiled once at startup
  2. Tight Loops: Fast for-loops instead of map/reduce
  3. Null Prototype Objects: Object.create(null) for outputs
  4. Skip Validation: Use mapRight/mapLeft when data is already validated

Benchmark (rough estimates):

  • Compilation: ~1-2ms (one-time cost)
  • Execution: ~0.1-0.5ms per object (thousands per second)
  • Zod validation: Usually the bottleneck, not mapping

Current Capabilities (v0.5)

Supported features:

  • ✅ Deep path mapping (user.profile.name)
  • ✅ Transforms with forward/backward functions
  • ✅ Built-in transforms (dates, currency, JSON, etc.)
  • ✅ Nested graft composition
  • ✅ Auto-mapping for matching fields

Planned features:

  • Array element mapping (apply grafts to array items)
  • Union type support (discriminated unions)

See the roadmap for upcoming features.

TypeScript Configuration

For best results, use strict mode:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true
  }
}

Migration Guide

From Manual Mapping

Before:

function dbToApi(db: UserDB): UserAPI {
  return {
    id: db.user_id,
    email: db.email_address,
    createdAt: db.created_at
  };
}

function apiToDb(api: UserAPI): UserDB {
  return {
    user_id: api.id,
    email_address: api.email,
    created_at: api.createdAt
  };
}

After:

const userGraft = createGraft(UserDB, UserAPI, fields => ({
  id: fields.user_id,
  email: fields.email_address,
  createdAt: fields.created_at
})).named('db', 'api');

// Use anywhere
const apiUser = userGraft.toApi(dbUser);
const dbUser = userGraft.toDb(apiUser);

License

MIT

See Also