@schemashift/joi-zod
v0.14.0
Published
Joi → Zod transformer for SchemaShift
Maintainers
Readme
@schemashift/joi-zod
Joi → Zod transformer for SchemaShift.
Installation
npm install @schemashift/joi-zodUsage
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.
// 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:
// 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 |
// 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.
// 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.
// 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().
// 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 — CLI tool for running migrations
- @schemashift/core — Core analysis engine
- @schemashift/yup-zod — Yup ↔ Zod transformer
License
MIT
