Skip to content

Migrating from Zod

A comprehensive guide to migrating from Zod to Kairo's schema validation system.

Why Migrate?

While Zod is excellent for schema validation, Kairo provides:

  • Result pattern integration - Consistent error handling across all operations
  • Three-pillar architecture - Validation integrated with HTTP and pipeline operations
  • Configuration objects - No method chaining, cleaner syntax
  • Native performance - Optimized validation without external dependencies
  • TypeScript inference - Better type safety and IDE support

Quick Migration

Before (Zod)

typescript
import { z } from 'zod'

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(2).max(100),
  email: z.string().email(),
  age: z.number().min(0).max(120),
  active: z.boolean().default(true)
})

try {
  const user = UserSchema.parse(userData)
  console.log('Valid user:', user)
} catch (error) {
  console.error('Validation error:', error.message)
}

After (Kairo)

typescript
import { data, Result } from '@sanzoku-labs/kairo'

const UserSchema = data.schema({
  id: { type: 'string', format: 'uuid' },
  name: { type: 'string', min: 2, max: 100 },
  email: { type: 'string', format: 'email' },
  age: { type: 'number', min: 0, max: 120 },
  active: { type: 'boolean', default: true }
})

const result = data.validate(userData, UserSchema)
Result.match(result, {
  Ok: user => console.log('Valid user:', user),
  Err: error => console.error('Validation error:', error.message)
})

Schema Definition Migration

Basic Types

typescript
// Zod
const BasicSchema = z.object({
  str: z.string(),
  num: z.number(),
  bool: z.boolean(),
  date: z.date(),
  arr: z.array(z.string()),
  obj: z.object({ nested: z.string() })
})

// Kairo
const BasicSchema = data.schema({
  str: { type: 'string' },
  num: { type: 'number' },
  bool: { type: 'boolean' },
  date: { type: 'date' },
  arr: { type: 'array', items: { type: 'string' } },
  obj: { type: 'object', properties: { nested: { type: 'string' } } }
})

String Validations

typescript
// Zod
const StringSchema = z.object({
  required: z.string(),
  optional: z.string().optional(),
  withDefault: z.string().default('default'),
  minLength: z.string().min(5),
  maxLength: z.string().max(50),
  length: z.string().length(10),
  email: z.string().email(),
  url: z.string().url(),
  uuid: z.string().uuid(),
  regex: z.string().regex(/^\d+$/),
  enum: z.enum(['a', 'b', 'c'])
})

// Kairo
const StringSchema = data.schema({
  required: { type: 'string' },
  optional: { type: 'string', optional: true },
  withDefault: { type: 'string', default: 'default' },
  minLength: { type: 'string', min: 5 },
  maxLength: { type: 'string', max: 50 },
  length: { type: 'string', length: 10 },
  email: { type: 'string', format: 'email' },
  url: { type: 'string', format: 'url' },
  uuid: { type: 'string', format: 'uuid' },
  regex: { type: 'string', pattern: '^\\d+$' },
  enum: { type: 'string', enum: ['a', 'b', 'c'] }
})

Number Validations

typescript
// Zod
const NumberSchema = z.object({
  basic: z.number(),
  integer: z.number().int(),
  positive: z.number().positive(),
  negative: z.number().negative(),
  min: z.number().min(0),
  max: z.number().max(100),
  multipleOf: z.number().multipleOf(5)
})

// Kairo
const NumberSchema = data.schema({
  basic: { type: 'number' },
  integer: { type: 'integer' },
  positive: { type: 'number', min: 0, exclusive: true },
  negative: { type: 'number', max: 0, exclusive: true },
  min: { type: 'number', min: 0 },
  max: { type: 'number', max: 100 },
  multipleOf: { type: 'number', multipleOf: 5 }
})

Array Validations

typescript
// Zod
const ArraySchema = z.object({
  stringArray: z.array(z.string()),
  numberArray: z.array(z.number()),
  minLength: z.array(z.string()).min(1),
  maxLength: z.array(z.string()).max(10),
  nonempty: z.array(z.string()).nonempty(),
  tuple: z.tuple([z.string(), z.number()])
})

// Kairo
const ArraySchema = data.schema({
  stringArray: { type: 'array', items: { type: 'string' } },
  numberArray: { type: 'array', items: { type: 'number' } },
  minLength: { type: 'array', items: { type: 'string' }, minItems: 1 },
  maxLength: { type: 'array', items: { type: 'string' }, maxItems: 10 },
  nonempty: { type: 'array', items: { type: 'string' }, minItems: 1 },
  tuple: { type: 'array', items: [{ type: 'string' }, { type: 'number' }] }
})

Object Validations

typescript
// Zod
const ObjectSchema = z.object({
  required: z.string(),
  optional: z.string().optional(),
  nested: z.object({
    inner: z.string()
  }),
  record: z.record(z.string()),
  partial: z.object({
    a: z.string(),
    b: z.number()
  }).partial()
})

// Kairo
const ObjectSchema = data.schema({
  required: { type: 'string' },
  optional: { type: 'string', optional: true },
  nested: {
    type: 'object',
    properties: {
      inner: { type: 'string' }
    }
  },
  record: { type: 'object', additionalProperties: { type: 'string' } },
  partial: {
    type: 'object',
    properties: {
      a: { type: 'string', optional: true },
      b: { type: 'number', optional: true }
    }
  }
})

Validation Migration

Parse vs Validate

typescript
// Zod
try {
  const user = UserSchema.parse(userData)
  // Success
} catch (error) {
  // Handle error
}

// Zod safe parse
const result = UserSchema.safeParse(userData)
if (result.success) {
  // Use result.data
} else {
  // Handle result.error
}

// Kairo
const result = data.validate(userData, UserSchema)
Result.match(result, {
  Ok: user => {
    // Success - use user
  },
  Err: error => {
    // Handle error
  }
})

Transform vs Coerce

typescript
// Zod
const CoerceSchema = z.object({
  stringToNumber: z.coerce.number(),
  stringToDate: z.coerce.date(),
  stringToBoolean: z.coerce.boolean()
})

// Kairo
const CoerceSchema = data.schema({
  stringToNumber: { type: 'number', coerce: true },
  stringToDate: { type: 'date', coerce: true },
  stringToBoolean: { type: 'boolean', coerce: true }
})

Refinements vs Custom Validation

typescript
// Zod
const RefinedSchema = z.object({
  password: z.string().refine(
    val => val.length >= 8 && /[A-Z]/.test(val),
    { message: 'Password must be at least 8 characters with uppercase' }
  ),
  email: z.string().email().refine(
    val => !val.includes('temp'),
    { message: 'Temporary emails not allowed' }
  )
})

// Kairo
const RefinedSchema = data.schema({
  password: {
    type: 'string',
    min: 8,
    pattern: '.*[A-Z].*',
    message: 'Password must be at least 8 characters with uppercase'
  },
  email: {
    type: 'string',
    format: 'email',
    validate: (value: string) => {
      if (value.includes('temp')) {
        return Result.Err('Temporary emails not allowed')
      }
      return Result.Ok(value)
    }
  }
})

Union Types and Discriminated Unions

Union Types

typescript
// Zod
const UnionSchema = z.union([
  z.string(),
  z.number(),
  z.boolean()
])

// Kairo
const UnionSchema = data.schema({
  type: 'union',
  oneOf: [
    { type: 'string' },
    { type: 'number' },
    { type: 'boolean' }
  ]
})

Discriminated Unions

typescript
// Zod
const DiscriminatedSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('user'), name: z.string() }),
  z.object({ type: z.literal('admin'), permissions: z.array(z.string()) })
])

// Kairo
const DiscriminatedSchema = data.schema({
  type: 'object',
  discriminator: 'type',
  oneOf: [
    {
      type: 'object',
      properties: {
        type: { type: 'string', const: 'user' },
        name: { type: 'string' }
      }
    },
    {
      type: 'object',
      properties: {
        type: { type: 'string', const: 'admin' },
        permissions: { type: 'array', items: { type: 'string' } }
      }
    }
  ]
})

Error Handling Migration

Zod Error Handling

typescript
// Zod
try {
  const user = UserSchema.parse(userData)
} catch (error) {
  if (error instanceof z.ZodError) {
    error.issues.forEach(issue => {
      console.log(`${issue.path.join('.')}: ${issue.message}`)
    })
  }
}

// Safe parse
const result = UserSchema.safeParse(userData)
if (!result.success) {
  result.error.issues.forEach(issue => {
    console.log(`${issue.path.join('.')}: ${issue.message}`)
  })
}

Kairo Error Handling

typescript
// Kairo
const result = data.validate(userData, UserSchema)
Result.match(result, {
  Ok: user => {
    // Success
  },
  Err: error => {
    console.log(`${error.path.join('.')}: ${error.message}`)
    
    // Access additional error info
    console.log('Field:', error.field)
    console.log('Expected:', error.expected)
    console.log('Received:', error.value)
  }
})

Integration with Service Layer

Zod with Manual Integration

typescript
// Zod
const fetchUser = async (id: string): Promise<User> => {
  const response = await fetch(`/api/users/${id}`)
  const data = await response.json()
  
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`)
  }
  
  try {
    return UserSchema.parse(data)
  } catch (error) {
    throw new Error(`Validation failed: ${error.message}`)
  }
}

Kairo with Integrated Validation

typescript
// Kairo
const fetchUser = async (id: string): Promise<Result<ServiceError, User>> => {
  return service.get(`/api/users/${id}`, {
    validate: UserSchema,
    timeout: 5000
  })
}

// Usage
const result = await fetchUser('123')
Result.match(result, {
  Ok: user => console.log('User:', user),
  Err: error => {
    switch (error.code) {
      case 'SERVICE_HTTP_ERROR':
        console.error('HTTP error:', error.status)
        break
      case 'DATA_VALIDATION_ERROR':
        console.error('Validation error:', error.message)
        break
      default:
        console.error('Unknown error:', error.message)
    }
  }
})

Advanced Migration Patterns

Recursive Schemas

typescript
// Zod
type Node = {
  id: string
  children: Node[]
}

const NodeSchema: z.ZodSchema<Node> = z.object({
  id: z.string(),
  children: z.lazy(() => z.array(NodeSchema))
})

// Kairo
const NodeSchema = data.schema({
  id: { type: 'string' },
  children: { type: 'array', items: { $ref: '#' } }
})

Schema Composition

typescript
// Zod
const BaseSchema = z.object({
  id: z.string(),
  createdAt: z.date()
})

const UserSchema = BaseSchema.extend({
  name: z.string(),
  email: z.string().email()
})

// Kairo
const BaseSchema = data.schema({
  id: { type: 'string' },
  createdAt: { type: 'date' }
})

const UserSchema = data.schema({
  ...BaseSchema.properties,
  name: { type: 'string' },
  email: { type: 'string', format: 'email' }
})

Performance Considerations

Zod Performance

typescript
// Zod - compiled for better performance
const CompiledSchema = z.object({
  name: z.string(),
  email: z.string().email()
}).strict()

// Still requires try/catch for each validation
const validate = (data: unknown) => {
  try {
    return CompiledSchema.parse(data)
  } catch (error) {
    throw error
  }
}

Kairo Performance

typescript
// Kairo - native validation with Result pattern
const Schema = data.schema({
  name: { type: 'string' },
  email: { type: 'string', format: 'email' }
})

// No try/catch needed, consistent Result pattern
const validate = (data: unknown) => {
  return data.validate(data, Schema)
}

Complete Migration Example

Before: Zod-based API

typescript
import { z } from 'zod'

const CreateUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  age: z.number().min(0).max(120).optional()
})

const UserResponseSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
  email: z.string().email(),
  age: z.number().optional(),
  createdAt: z.date()
})

class UserService {
  async createUser(userData: unknown): Promise<User> {
    // Validate input
    const validatedData = CreateUserSchema.parse(userData)
    
    // Make API call
    const response = await fetch('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(validatedData)
    })
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`)
    }
    
    const responseData = await response.json()
    
    // Validate response
    return UserResponseSchema.parse(responseData)
  }
}

After: Kairo-based API

typescript
import { service, data, Result } from '@sanzoku-labs/kairo'

const CreateUserSchema = data.schema({
  name: { type: 'string', min: 2, max: 100 },
  email: { type: 'string', format: 'email' },
  age: { type: 'number', min: 0, max: 120, optional: true }
})

const UserResponseSchema = data.schema({
  id: { type: 'string', format: 'uuid' },
  name: { type: 'string' },
  email: { type: 'string', format: 'email' },
  age: { type: 'number', optional: true },
  createdAt: { type: 'date' }
})

class UserService {
  async createUser(userData: unknown): Promise<Result<ServiceError | DataError, User>> {
    // Validate input
    const validationResult = data.validate(userData, CreateUserSchema)
    if (Result.isErr(validationResult)) {
      return validationResult
    }
    
    // Make API call with automatic response validation
    return service.post('/api/users', {
      body: validationResult.value,
      validate: UserResponseSchema,
      timeout: 5000
    })
  }
}

// Usage
const userService = new UserService()
const result = await userService.createUser(userData)

Result.match(result, {
  Ok: user => console.log('User created:', user),
  Err: error => {
    switch (error.code) {
      case 'DATA_VALIDATION_ERROR':
        console.error('Validation error:', error.message)
        break
      case 'SERVICE_HTTP_ERROR':
        console.error('HTTP error:', error.status)
        break
      default:
        console.error('Unknown error:', error.message)
    }
  }
})

Migration Checklist

  • [ ] Replace Zod imports with Kairo data imports
  • [ ] Convert schema definitions to Kairo format
  • [ ] Replace parse/safeParse with validate and Result pattern
  • [ ] Update error handling to use structured errors
  • [ ] Integrate schemas with service layer validation
  • [ ] Update TypeScript types for Result pattern
  • [ ] Remove try/catch blocks in favor of Result matching
  • [ ] Test validation performance improvements

Next Steps

Released under the MIT License.