Files
gruperly/.agents/skills/zod/references/compose-preprocess.md
2026-09-04 16:49:24 -03:00

143 lines
3.6 KiB
Markdown

---
title: Use preprocess() for Data Normalization
impact: MEDIUM
impactDescription: Validating before cleaning data causes false rejections; preprocess() normalizes input before schema validation runs
tags: compose, preprocess, normalize, cleaning
---
## Use preprocess() for Data Normalization
When incoming data needs normalization before validation (trimming whitespace, parsing JSON strings, converting formats), use `z.preprocess()`. This runs a function on the raw input before Zod's type checking, allowing you to clean data that would otherwise fail validation.
**Incorrect (validation fails on unnormalized data):**
```typescript
import { z } from 'zod'
const userSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
config: z.object({
theme: z.string(),
}),
})
// Raw form data
const formData = {
name: ' John Doe ', // Has whitespace
email: 'JOHN@EXAMPLE.COM', // Uppercase
config: '{"theme": "dark"}', // JSON string, not object
}
userSchema.parse(formData)
// ZodError: Expected object, received string at "config"
```
**Correct (using preprocess):**
```typescript
import { z } from 'zod'
// Preprocess normalizes before validation
const trimmedString = z.preprocess(
(val) => (typeof val === 'string' ? val.trim() : val),
z.string()
)
const lowercaseEmail = z.preprocess(
(val) => (typeof val === 'string' ? val.toLowerCase().trim() : val),
z.string().email()
)
const jsonObject = z.preprocess(
(val) => {
if (typeof val === 'string') {
try {
return JSON.parse(val)
} catch {
return val // Let Zod report the error
}
}
return val
},
z.object({ theme: z.string() })
)
const userSchema = z.object({
name: trimmedString.pipe(z.string().min(1)),
email: lowercaseEmail,
config: jsonObject,
})
const formData = {
name: ' John Doe ',
email: 'JOHN@EXAMPLE.COM',
config: '{"theme": "dark"}',
}
const user = userSchema.parse(formData)
// { name: 'John Doe', email: 'john@example.com', config: { theme: 'dark' } }
```
**Common preprocessing patterns:**
```typescript
// Trim all strings
const trimmedString = z.preprocess(
(val) => (typeof val === 'string' ? val.trim() : val),
z.string()
)
// Parse numeric strings
const numericString = z.preprocess(
(val) => (typeof val === 'string' ? Number(val) : val),
z.number()
)
// Parse boolean-like values
const booleanLike = z.preprocess(
(val) => {
if (val === 'true' || val === '1' || val === 1) return true
if (val === 'false' || val === '0' || val === 0) return false
return val
},
z.boolean()
)
// Parse date strings
const dateString = z.preprocess(
(val) => (typeof val === 'string' ? new Date(val) : val),
z.date()
)
// Split comma-separated strings into arrays
const csvArray = z.preprocess(
(val) => (typeof val === 'string' ? val.split(',').map(s => s.trim()) : val),
z.array(z.string())
)
```
**Preprocess vs Transform:**
```typescript
// preprocess() runs BEFORE type checking
// Use for: Normalizing input format before validation
z.preprocess(val => String(val).trim(), z.string().min(1))
// transform() runs AFTER type checking
// Use for: Converting validated data to different format
z.string().transform(s => s.toUpperCase())
// Order of operations:
// 1. preprocess receives raw unknown input
// 2. Zod validates the preprocessed value
// 3. transform converts the validated value
```
**When NOT to use this pattern:**
- When `.coerce` methods handle the conversion (simpler)
- When transformation should happen after validation (use transform)
- When normalization could hide validation errors
Reference: [Zod API - preprocess](https://zod.dev/api#preprocess)