143 lines
3.6 KiB
Markdown
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)
|