npm package: @schemashift/joi-zod
Description: Joi → Zod transformer for SchemaShift
Author: Joseph May
Latest version: 0.14.0

# @schemashift/joi-zod

Joi → Zod transformer for SchemaShift.

[![npm version](https://img.shields.io/npm/v/@schemashift/joi-zod.svg)](https://www.npmjs.com/package/@schemashift/joi-zod)
[![npm downloads](https://img.shields.io/npm/dm/@schemashift/joi-zod.svg)](https://www.npmjs.com/package/@schemashift/joi-zod)
[![CI](https://github.com/jdmay2/schemashift/actions/workflows/ci.yml/badge.svg)](https://github.com/jdmay2/schemashift/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)

## Installation

```bash
npm install @schemashift/joi-zod
```

## Usage

```typescript
import { createJoiToZodHandler } from '@schemashift/joi-zod';
import { TransformEngine } from '@schemashift/core';

const engine = new TransformEngine();
engine.registerHandler('joi', 'zod', createJoiToZodHandler());
```

## Transformation Mappings

### Imports

| Joi | Zod |
|-----|-----|
| `import Joi from 'joi'` | `import { z } from 'zod'` |
| `import Joi from '@hapi/joi'` | `import { z } from 'zod'` |

### Basic Types

| Joi | Zod |
|-----|-----|
| `Joi.string()` | `z.string()` |
| `Joi.number()` | `z.number()` |
| `Joi.boolean()` | `z.boolean()` |
| `Joi.date()` | `z.date()` |
| `Joi.array()` | `z.array()` |
| `Joi.object()` | `z.object()` |
| `Joi.any()` | `z.unknown()` |
| `Joi.binary()` | `z.instanceof(Buffer)` |
| `Joi.func()` | `z.function()` |
| `Joi.symbol()` | `z.symbol()` |

### Required/Optional

| Joi | Zod | Notes |
|-----|-----|-------|
| `.required()` | (removed) | Zod fields are required by default |
| `.optional()` | `.optional()` | |
| `.allow(null)` | `.nullable()` | |
| `.forbidden()` | `z.never()` | |

### String Validations

| Joi | Zod |
|-----|-----|
| `.email()` | `.email()` |
| `.uri()` | `.url()` |
| `.uuid()` | `.uuid()` |
| `.ip()` | `.ip()` |
| `.min(n)` | `.min(n)` |
| `.max(n)` | `.max(n)` |
| `.length(n)` | `.length(n)` |
| `.pattern(regex)` | `.regex(regex)` |
| `.regex(regex)` | `.regex(regex)` |
| `.alphanum()` | `.regex(/^[a-zA-Z0-9]+$/)` |
| `.lowercase()` | `.toLowerCase()` |
| `.uppercase()` | `.toUpperCase()` |
| `.trim()` | `.trim()` |

### Number Validations

| Joi | Zod |
|-----|-----|
| `.min(n)` | `.min(n)` |
| `.max(n)` | `.max(n)` |
| `.greater(n)` | `.gt(n)` |
| `.less(n)` | `.lt(n)` |
| `.positive()` | `.positive()` |
| `.negative()` | `.negative()` |
| `.integer()` | `.int()` |
| `.port()` | `.int().min(0).max(65535)` |
| `.precision(n)` | `.multipleOf(Math.pow(10, -n))` |

### Array Validations

| Joi | Zod |
|-----|-----|
| `.min(n)` | `.min(n)` |
| `.max(n)` | `.max(n)` |
| `.length(n)` | `.length(n)` |
| `.items(schema)` | `z.array(schema)` |
| `.unique()` | `.refine(...)` |
| `.has(schema)` | `.refine(...)` |

### Object Methods

| Joi | Zod |
|-----|-----|
| `.keys({...})` | `z.object({...})` |
| `.pattern(key, value)` | `z.record(key, value)` |
| `.unknown()` | `.passthrough()` |
| `.strip()` | `.strip()` |

### Alternatives

| Joi | Zod |
|-----|-----|
| `Joi.alternatives()` | `z.union()` |
| `.try(a, b)` | `z.union([a, b])` |

### Other

| Joi | Zod |
|-----|-----|
| `.default(value)` | `.default(value)` |
| `.valid(...values)` | `z.enum([...])` or `z.literal()` |
| `.invalid(...values)` | `.refine(v => !values.includes(v))` |
| `.custom(fn)` | `.refine(fn)` |

## Patterns Requiring Manual Review

The transformer generates actionable `.superRefine()` scaffolding for these patterns:

### `.when()` Conditionals

Joi's `.when()` generates a `.superRefine()` template with the original condition context.

```typescript
// Joi
const schema = Joi.object({
  type: Joi.string().valid('personal', 'business'),
  companyName: Joi.string().when('type', {
    is: 'business',
    then: Joi.string().required(),
    otherwise: Joi.string().optional(),
  }),
});

// Zod (scaffolded output)
const schema = z.object({
  type: z.enum(['personal', 'business']),
  companyName: z.string().optional(),
}).superRefine((data, ctx) => {
  if (data.type === 'business' && !data.companyName) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: 'Company name required',
      path: ['companyName'],
    });
  }
});
```

### `.with()` / `.without()` Dependencies

Joi's object dependency methods generate `.superRefine()` scaffolding with the dependency logic:

```typescript
// Joi
Joi.object({
  a: Joi.string(),
  b: Joi.string(),
}).with('a', 'b')  // if 'a' present, 'b' required

// Zod (scaffolded)
z.object({
  a: z.string().optional(),
  b: z.string().optional(),
}).superRefine((data, ctx) => {
  if (data.a && !data.b) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: 'b is required when a is present',
      path: ['b'],
    });
  }
});
```

### Co-Dependency Methods (`.and()`, `.nand()`, `.or()`, `.xor()`)

These Joi methods generate `.superRefine()` templates with the correct conditional logic:

| Method | Generated Logic |
|--------|----------------|
| `.and('a', 'b')` | Check all fields present or all absent |
| `.nand('a', 'b')` | Check not all fields present simultaneously |
| `.or('a', 'b')` | Check at least one field present |
| `.xor('a', 'b')` | Check exactly one field present |

```typescript
// Joi
Joi.object({ a: Joi.string(), b: Joi.string() }).xor('a', 'b')

// Zod (scaffolded)
z.object({ a: z.string().optional(), b: z.string().optional() })
  .superRefine((data, ctx) => {
    /* TODO(schemashift): .xor('a', 'b')
       Exactly one of the fields must be present.
       if ([data.a, data.b].filter(v => v !== undefined).length !== 1) {
         ctx.addIssue({ code: 'custom', message: 'Exactly one of a, b must be provided' });
       }
    */
  });
```

### `.external()` Async Validation

Joi's `.external()` for async validation needs conversion.

```typescript
// Joi
Joi.string().external(async (value) => {
  const exists = await checkExists(value);
  if (exists) throw new Error('Already exists');
  return value;
});

// Zod
z.string().refine(async (value) => {
  return !(await checkExists(value));
}, 'Already exists');
// Use .parseAsync() for validation
```

### `.messages()` Custom Messages

Joi's `.messages()` for custom error messages.

```typescript
// Joi
Joi.string().min(3).messages({
  'string.min': 'Must be at least 3 characters',
});

// Zod
z.string().min(3, 'Must be at least 3 characters');
```

### Reference / Link

Joi's `Joi.ref()` and `Joi.link()` for cross-field references need `.superRefine()`.

```typescript
// Joi
Joi.object({
  password: Joi.string(),
  confirmPassword: Joi.string().valid(Joi.ref('password')),
});

// Zod
z.object({
  password: z.string(),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: 'Passwords must match',
  path: ['confirmPassword'],
});
```

## Related Packages

- [schemashift-cli](https://www.npmjs.com/package/schemashift-cli) — CLI tool for running migrations
- [@schemashift/core](https://www.npmjs.com/package/@schemashift/core) — Core analysis engine
- [@schemashift/yup-zod](https://www.npmjs.com/package/@schemashift/yup-zod) — Yup ↔ Zod transformer

## License

MIT