> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/colinhacks/zod/llms.txt
> Use this file to discover all available pages before exploring further.

# Refinements

> Add custom validation logic with .refine() and .superRefine() methods

## Overview

**Refinements** allow you to add custom validation logic beyond Zod's built-in validators. While transformations modify data, refinements validate data without changing it.

## Basic Refinement with .refine()

Use `.refine()` to add custom validation:

```typescript theme={null}
import { z } from 'zod';

const schema = z.string().refine(
  (val) => val.length <= 10,
  'String must be 10 characters or less'
);

schema.parse('hello');     // OK
schema.parse('hello world'); // Error: String must be 10 characters or less
```

From `packages/zod/src/v4/classic/schemas.ts:92-95`, the `.refine()` signature:

```typescript theme={null}
refine<Ch extends (arg: Output) => unknown | Promise<unknown>>(
  check: Ch,
  params?: string | $ZodCustomParams
): Ch extends (arg: any) => arg is infer R ? this & ZodType<R, Input> : this
```

## Custom Error Messages

Provide custom error messages:

```typescript theme={null}
// String message
const schema1 = z.string().refine(
  (val) => val.includes('@'),
  'Must contain @ symbol'
);

// Object with message
const schema2 = z.string().refine(
  (val) => val.includes('@'),
  { message: 'Must contain @ symbol' }
);

// Dynamic message
const schema3 = z.number().refine(
  (val) => val >= 0,
  (val) => ({ message: `Expected non-negative, got ${val}` })
);
```

From `packages/zod/src/v4/classic/tests/refine.test.ts:54-75`:

```typescript theme={null}
const validationSchema = z.object({
  email: z.string().email(),
  password: z.string(),
  confirmPassword: z.string(),
}).refine(
  (data) => data.password === data.confirmPassword,
  'Both password and confirmation must match'
);

const result = validationSchema.safeParse({
  email: 'aaaa@gmail.com',
  password: 'aaaaaaaa',
  confirmPassword: 'bbbbbbbb',
});

expect(result.success).toEqual(false);
if (!result.success) {
  expect(result.error.issues[0].message).toEqual(
    'Both password and confirmation must match'
  );
}
```

## Refine on Objects

Refinements are powerful for cross-field validation:

```typescript theme={null}
const PasswordSchema = z.object({
  password: z.string().min(8),
  confirmPassword: z.string(),
}).refine(
  (data) => data.password === data.confirmPassword,
  {
    message: 'Passwords do not match',
    path: ['confirmPassword'], // Error will point to this field
  }
);
```

From `packages/zod/src/v4/classic/tests/refine.test.ts:169-181`:

```typescript theme={null}
const result = z.object({ password: z.string(), confirm: z.string() })
  .refine(
    (data) => data.confirm === data.password,
    { path: ['confirm'] }
  )
  .safeParse({ password: 'asdf', confirm: 'qewr' });

expect(result.success).toEqual(false);
if (!result.success) {
  expect(result.error.issues[0].path).toEqual(['confirm']);
}
```

### Error Path

The `path` option specifies where the error should appear:

```typescript theme={null}
const schema = z.object({
  start: z.date(),
  end: z.date(),
}).refine(
  (data) => data.end > data.start,
  {
    message: 'End date must be after start date',
    path: ['end'], // Error appears on 'end' field
  }
);
```

## Async Refinements

Refinements can be asynchronous for database checks, API calls, etc:

```typescript theme={null}
const schema = z.string().refine(
  async (email) => {
    // Check if email exists in database
    const exists = await checkEmailExists(email);
    return !exists;
  },
  { message: 'Email already exists' }
);

// Must use parseAsync for async refinements
const result = await schema.parseAsync('test@example.com');
```

From `packages/zod/src/v4/classic/tests/refine.test.ts:77-108`:

```typescript theme={null}
const validationSchema = z.object({
  email: z.string().email(),
  password: z.string(),
  confirmPassword: z.string(),
}).refine(
  (data) => Promise.resolve().then(() => data.password === data.confirmPassword),
  'Both password and confirmation must match'
);

// Should pass with matching passwords
const validData = {
  email: 'aaaa@gmail.com',
  password: 'password',
  confirmPassword: 'password',
};

await expect(validationSchema.parseAsync(validData)).resolves.toEqual(validData);

// Should fail with non-matching passwords
await expect(
  validationSchema.parseAsync({
    email: 'aaaa@gmail.com',
    password: 'password',
    confirmPassword: 'different',
  })
).rejects.toThrow();
```

<Warning>
  Async refinements require `parseAsync()` or `safeParseAsync()`. Using `parse()` will throw an error.
</Warning>

## superRefine() for Advanced Validation

For complex validation with multiple errors, use `.superRefine()`:

```typescript theme={null}
const schema = z.array(z.string()).superRefine((val, ctx) => {
  // Check length
  if (val.length > 3) {
    ctx.addIssue({
      code: z.ZodIssueCode.too_big,
      maximum: 3,
      inclusive: true,
      type: 'array',
      message: 'Too many items',
    });
  }
  
  // Check for duplicates
  if (val.length !== new Set(val).size) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: 'Array contains duplicates',
    });
  }
});
```

From `packages/zod/src/v4/classic/schemas.ts:96-98`, the `.superRefine()` signature:

```typescript theme={null}
superRefine(
  refinement: (arg: Output, ctx: $RefinementCtx) => void | Promise<void>
): this
```

From `packages/zod/src/v4/classic/tests/refine.test.ts:183-215`:

```typescript theme={null}
const Strings = z.array(z.string()).superRefine((val, ctx) => {
  if (val.length > 3) {
    ctx.addIssue({
      input: val,
      code: 'too_big',
      origin: 'array',
      maximum: 3,
      inclusive: true,
      exact: true,
      message: 'Too many items 😡',
    });
  }

  if (val.length !== new Set(val).size) {
    ctx.addIssue({
      input: val,
      code: 'custom',
      message: `No duplicates allowed.`,
    });
  }
});

const result = Strings.safeParse(['asdf', 'asdf', 'asdf', 'asdf']);
expect(result.success).toBe(false);
if (!result.success) {
  expect(result.error.issues.length).toBe(2);
  expect(result.error.issues[0].message).toBe('Too many items 😡');
  expect(result.error.issues[1].message).toBe('No duplicates allowed.');
}
```

### Adding Multiple Issues

`.superRefine()` allows reporting multiple validation errors:

```typescript theme={null}
const PasswordStrength = z.string().superRefine((val, ctx) => {
  if (val.length < 8) {
    ctx.addIssue({
      code: z.ZodIssueCode.too_small,
      minimum: 8,
      type: 'string',
      inclusive: true,
      message: 'Password must be at least 8 characters',
    });
  }
  
  if (!/[A-Z]/.test(val)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: 'Password must contain uppercase letter',
    });
  }
  
  if (!/[0-9]/.test(val)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: 'Password must contain a number',
    });
  }
});
```

## Early Termination

Control validation flow with early termination options:

### Using `fatal: true`

```typescript theme={null}
const schema = z.string().superRefine((val, ctx) => {
  if (val.length < 2) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      fatal: true,  // Stop validation immediately
      message: 'Too short',
    });
  }
}).refine(() => false); // This won't run if fatal issue occurs
```

From `packages/zod/src/v4/classic/tests/refine.test.ts:133-154`:

```typescript theme={null}
const schema = z.string()
  .superRefine((val, ctx) => {
    if (val.length < 2) {
      ctx.addIssue({
        code: 'custom',
        fatal: true,
        message: 'BAD',
      });
    }
  })
  .refine(() => false);

const result = schema.safeParse('');
expect(result.success).toEqual(false);
if (!result.success) {
  expect(result.error.issues.length).toEqual(1);
  expect(result.error.issues[0].message).toEqual('BAD');
}
```

### Using `continue: false`

```typescript theme={null}
const schema = z.string().superRefine((val, ctx) => {
  if (val.length < 2) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      continue: false, // Stop this refinement, but continue others
      message: 'BAD',
    });
  }
});
```

### Using `abort` in .refine()

```typescript theme={null}
const schema = z.string()
  .refine(() => false, { abort: true }) // Abort on failure
  .refine(() => false);                   // This won't run
```

From `packages/zod/src/v4/classic/tests/refine.test.ts:155-167`:

```typescript theme={null}
const schema = z.string()
  .refine(() => false, { abort: true })
  .refine(() => false);

const result = schema.safeParse('');
expect(result.success).toEqual(false);
if (!result.success) {
  expect(result.error.issues.length).toEqual(1);
}
```

## Type Narrowing with Refinements

Refinements can narrow TypeScript types using type predicates:

```typescript theme={null}
const isValidEmail = (val: string): val is `${string}@${string}` => {
  return val.includes('@');
};

const schema = z.string().refine(isValidEmail);

type Input = z.input<typeof schema>;   // string
type Output = z.output<typeof schema>; // `${string}@${string}`
```

From `packages/zod/src/v4/classic/tests/refine.test.ts:422-432`:

```typescript theme={null}
// Type predicate narrows the type
const schema = z.string().refine((val): val is 'a' => val === 'a');

type Input = z.input<typeof schema>;   // string
type Output = z.output<typeof schema>; // 'a'

// Without type predicate, type doesn't narrow
const schema2 = z.string().refine((val) => val === 'a');

type Input2 = z.input<typeof schema2>;   // string
type Output2 = z.output<typeof schema2>; // string (not narrowed)
```

## Practical Examples

### Email Uniqueness Check

```typescript theme={null}
const UniqueEmail = z.string()
  .email()
  .refine(
    async (email) => {
      const user = await db.users.findOne({ email });
      return !user;
    },
    { message: 'Email already exists' }
  );
```

### Password Confirmation

```typescript theme={null}
const RegistrationSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  confirmPassword: z.string(),
}).refine(
  (data) => data.password === data.confirmPassword,
  {
    message: 'Passwords do not match',
    path: ['confirmPassword'],
  }
);
```

### Date Range Validation

```typescript theme={null}
const EventSchema = z.object({
  title: z.string(),
  startDate: z.date(),
  endDate: z.date(),
}).refine(
  (data) => data.endDate > data.startDate,
  {
    message: 'End date must be after start date',
    path: ['endDate'],
  }
);
```

### Complex Business Logic

```typescript theme={null}
const OrderSchema = z.object({
  items: z.array(z.object({
    id: z.string(),
    quantity: z.number().positive(),
    price: z.number().positive(),
  })),
  discount: z.number().min(0).max(1),
  shippingCost: z.number().nonnegative(),
}).superRefine((data, ctx) => {
  const subtotal = data.items.reduce(
    (sum, item) => sum + item.quantity * item.price,
    0
  );
  
  const total = subtotal * (1 - data.discount) + data.shippingCost;
  
  // Minimum order value
  if (total < 10) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: 'Order total must be at least $10',
      path: ['items'],
    });
  }
  
  // Free shipping threshold
  if (subtotal >= 100 && data.shippingCost > 0) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: 'Orders over $100 qualify for free shipping',
      path: ['shippingCost'],
    });
  }
});
```

### Credit Card Validation

```typescript theme={null}
const CreditCardSchema = z.string()
  .regex(/^\d{16}$/, 'Must be 16 digits')
  .refine(
    (num) => {
      // Luhn algorithm
      let sum = 0;
      let isEven = false;
      
      for (let i = num.length - 1; i >= 0; i--) {
        let digit = parseInt(num[i], 10);
        
        if (isEven) {
          digit *= 2;
          if (digit > 9) {
            digit -= 9;
          }
        }
        
        sum += digit;
        isEven = !isEven;
      }
      
      return sum % 10 === 0;
    },
    { message: 'Invalid credit card number' }
  );
```

## Refinements vs Transformations

<Note>
  **Key Difference:**

  * **Refinements** validate data without changing it
  * **Transformations** modify data after validation
</Note>

```typescript theme={null}
// Refinement: validates, doesn't change value
const refined = z.string().refine((val) => val.length > 5);
type RefinedOutput = z.output<typeof refined>; // string
const result1 = refined.parse('hello world'); // 'hello world'

// Transformation: changes the value
const transformed = z.string().transform((val) => val.length);
type TransformedOutput = z.output<typeof transformed>; // number
const result2 = transformed.parse('hello world'); // 11
```

## Chaining Refinements

Multiple refinements can be chained:

```typescript theme={null}
const schema = z.number()
  .refine((n) => n >= 0, 'Must be non-negative')
  .refine((n) => n <= 100, 'Must be at most 100')
  .refine((n) => n % 5 === 0, 'Must be multiple of 5');
```

## Performance Considerations

<Warning>
  Async refinements can be slow. Consider caching or batching database lookups for better performance.
</Warning>

```typescript theme={null}
// Slow: Database call for each value
const slow = z.string().refine(async (val) => {
  return await db.check(val);
});

// Better: Batch validation
const better = z.array(z.string()).refine(async (values) => {
  const results = await db.batchCheck(values);
  return results.every((r) => r.valid);
});
```

## Best Practices

1. **Use built-in validators first** - They're optimized and well-tested
2. **Keep refinements focused** - One validation per refinement
3. **Use .superRefine() for multiple errors** - Better UX than stopping at first error
4. **Set appropriate error paths** - Help users fix the right field
5. **Consider async performance** - Cache or batch when possible
6. **Use type predicates for narrowing** - Get better TypeScript types

```typescript theme={null}
// Good: Focused, clear refinements
const schema = z.object({
  email: z.string().email(),
  age: z.number().positive(),
}).refine(
  (data) => data.age >= 18,
  { message: 'Must be 18 or older', path: ['age'] }
);

// Bad: Everything in one refinement
const bad = z.object({
  email: z.string(),
  age: z.number(),
}).refine((data) => {
  return data.email.includes('@') && data.age >= 18;
}, 'Invalid data'); // Unclear what's wrong
```

## Common Patterns

### Conditional Validation

```typescript theme={null}
const schema = z.object({
  country: z.string(),
  zipCode: z.string(),
}).refine(
  (data) => {
    if (data.country === 'US') {
      return /^\d{5}(-\d{4})?$/.test(data.zipCode);
    }
    return true; // Skip validation for other countries
  },
  {
    message: 'Invalid US zip code',
    path: ['zipCode'],
  }
);
```

### Dependent Fields

```typescript theme={null}
const schema = z.object({
  hasAccount: z.boolean(),
  accountId: z.string().optional(),
}).refine(
  (data) => {
    if (data.hasAccount) {
      return data.accountId !== undefined;
    }
    return true;
  },
  {
    message: 'Account ID required when "has account" is checked',
    path: ['accountId'],
  }
);
```

## Next Steps

* Learn about [Transformations](/concepts/transformations) to modify data during parsing
* Explore [Parsing](/concepts/parsing) to understand error handling
* Master [Type Inference](/concepts/type-inference) for type narrowing with refinements
