or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

advanced-schemas.mdcoercion.mdcollections.mderrors.mdindex.mdiso-datetime.mdjson-schema.mdlocales.mdnumber-formats.mdparsing.mdprimitives.mdrefinements.mdstring-formats.mdtransformations.mdunions-intersections.mdutilities.mdwrappers.md

refinements.mddocs/

0

# Refinements and Custom Validation

1

2

Add custom validation logic to schemas with synchronous or asynchronous checks.

3

4

## Refine Method

5

6

Boolean validation checks with custom error messages.

7

8

```typescript { .api }

9

interface ZodType<Output, Input> {

10

refine(

11

check: (value: Output) => boolean | Promise<boolean>,

12

params?: string | {

13

message?: string;

14

path?: (string | number)[];

15

params?: object;

16

}

17

): this;

18

}

19

```

20

21

**Examples:**

22

```typescript

23

// Simple refine

24

z.number().refine((val) => val > 0, "Must be positive")

25

z.string().refine((val) => val.includes("@"), "Invalid email format")

26

27

// With custom path

28

z.string().refine(

29

(val) => val.length >= 8,

30

{

31

message: "Must be at least 8 characters",

32

path: ["credentials", "password"],

33

}

34

)

35

36

// Multiple refine calls

37

z.string()

38

.refine((val) => val.length >= 8, "At least 8 characters")

39

.refine((val) => /[A-Z]/.test(val), "Must contain uppercase")

40

.refine((val) => /[a-z]/.test(val), "Must contain lowercase")

41

.refine((val) => /[0-9]/.test(val), "Must contain number")

42

.refine((val) => /[!@#$%^&*]/.test(val), "Must contain special char")

43

44

// Refine on objects (cross-field validation)

45

z.object({

46

password: z.string(),

47

confirmPassword: z.string(),

48

}).refine(

49

(data) => data.password === data.confirmPassword,

50

{

51

message: "Passwords don't match",

52

path: ["confirmPassword"],

53

}

54

)

55

56

// Async refine (requires parseAsync)

57

z.string().refine(

58

async (username) => {

59

const exists = await checkUsernameExists(username);

60

return !exists;

61

},

62

{ message: "Username already taken" }

63

)

64

65

// Must use:

66

await schema.parseAsync(data);

67

```

68

69

## SuperRefine Method

70

71

Advanced refinement with context for adding multiple issues.

72

73

```typescript { .api }

74

interface ZodType<Output, Input> {

75

superRefine(

76

refineFn: (value: Output, ctx: RefinementCtx) => void | Promise<void>

77

): this;

78

}

79

80

interface RefinementCtx {

81

addIssue(issue: IssueData): void;

82

path: (string | number)[];

83

}

84

85

interface IssueData {

86

code: ZodIssueCode;

87

message?: string;

88

path?: (string | number)[];

89

fatal?: boolean; // Stops validation immediately

90

[key: string]: any;

91

}

92

```

93

94

**Examples:**

95

```typescript

96

// Multiple validation issues

97

z.string().superRefine((val, ctx) => {

98

if (val.length < 8) {

99

ctx.addIssue({

100

code: z.ZodIssueCode.too_small,

101

minimum: 8,

102

type: "string",

103

inclusive: true,

104

message: "At least 8 characters",

105

});

106

}

107

108

if (!/[A-Z]/.test(val)) {

109

ctx.addIssue({

110

code: z.ZodIssueCode.custom,

111

message: "Must contain uppercase",

112

});

113

}

114

115

if (!/[0-9]/.test(val)) {

116

ctx.addIssue({

117

code: z.ZodIssueCode.custom,

118

message: "Must contain number",

119

});

120

}

121

})

122

123

// Conditional validation

124

z.object({

125

type: z.enum(["personal", "business"]),

126

taxId: z.string().optional(),

127

}).superRefine((data, ctx) => {

128

if (data.type === "business" && !data.taxId) {

129

ctx.addIssue({

130

code: z.ZodIssueCode.custom,

131

message: "Tax ID required for business accounts",

132

path: ["taxId"],

133

});

134

}

135

})

136

137

// Cross-field validation

138

z.object({

139

startDate: z.date(),

140

endDate: z.date(),

141

}).superRefine((data, ctx) => {

142

if (data.endDate <= data.startDate) {

143

ctx.addIssue({

144

code: z.ZodIssueCode.custom,

145

message: "End date must be after start date",

146

path: ["endDate"],

147

});

148

}

149

})

150

151

// Fatal issues (stop validation immediately)

152

z.string().superRefine((val, ctx) => {

153

if (val.includes("forbidden")) {

154

ctx.addIssue({

155

code: z.ZodIssueCode.custom,

156

message: "Forbidden content detected",

157

fatal: true, // Stops validation

158

});

159

}

160

})

161

162

// Async super refine

163

z.string().superRefine(async (val, ctx) => {

164

const isValid = await validateWithAPI(val);

165

if (!isValid) {

166

ctx.addIssue({

167

code: z.ZodIssueCode.custom,

168

message: "Validation failed",

169

});

170

}

171

})

172

```

173

174

## Check Method

175

176

Reusable validation check functions.

177

178

```typescript { .api }

179

interface ZodType<Output, Input> {

180

check(...checks: Array<(value: Output) => boolean | Promise<boolean>>): this;

181

}

182

```

183

184

**Examples:**

185

```typescript

186

// Single check

187

z.number().check((val) => val % 2 === 0)

188

189

// Multiple checks

190

z.number()

191

.check((val) => val > 0)

192

.check((val) => val < 100)

193

.check((val) => val % 2 === 0)

194

195

// Reusable check functions

196

const isPositive = (val: number) => val > 0;

197

const isEven = (val: number) => val % 2 === 0;

198

199

z.number().check(isPositive, isEven)

200

```

201

202

## Overwrite Method

203

204

Transform values during validation (in-place modification).

205

206

```typescript { .api }

207

interface ZodType<Output, Input> {

208

overwrite(transformFn: (value: Output) => Output): ZodEffects<this>;

209

}

210

```

211

212

**Examples:**

213

```typescript

214

// Normalize email

215

z.string()

216

.email()

217

.overwrite((val) => val.toLowerCase().trim())

218

219

// Remove forbidden characters

220

z.string()

221

.overwrite((val) => val.replace(/[<>]/g, ""))

222

223

// Conditional overwrite

224

z.object({

225

name: z.string(),

226

autoGenerate: z.boolean(),

227

}).overwrite((data) => {

228

if (data.autoGenerate) {

229

return { ...data, name: `Generated_${Date.now()}` };

230

}

231

return data;

232

})

233

```

234

235

## Common Patterns

236

237

```typescript

238

// Password validation

239

const PasswordSchema = z

240

.string()

241

.refine((val) => val.length >= 8, "At least 8 characters")

242

.refine((val) => /[A-Z]/.test(val), "Must contain uppercase")

243

.refine((val) => /[a-z]/.test(val), "Must contain lowercase")

244

.refine((val) => /[0-9]/.test(val), "Must contain number");

245

246

// Confirm password pattern

247

const PasswordFormSchema = z

248

.object({

249

password: z.string().min(8),

250

confirmPassword: z.string(),

251

})

252

.refine((data) => data.password === data.confirmPassword, {

253

message: "Passwords don't match",

254

path: ["confirmPassword"],

255

});

256

257

// Async username availability

258

const UsernameSchema = z.string().refine(

259

async (username) => {

260

const available = await checkAvailability(username);

261

return available;

262

},

263

{ message: "Username already taken" }

264

);

265

266

// Must use: await schema.parseAsync(data);

267

268

// Date range validation

269

const DateRangeSchema = z

270

.object({

271

start: z.date(),

272

end: z.date(),

273

})

274

.refine((data) => data.end > data.start, {

275

message: "End date must be after start date",

276

path: ["end"],

277

});

278

279

// Conditional required field

280

const AccountSchema = z

281

.object({

282

type: z.enum(["personal", "business"]),

283

taxId: z.string().optional(),

284

})

285

.superRefine((data, ctx) => {

286

if (data.type === "business" && !data.taxId) {

287

ctx.addIssue({

288

code: z.ZodIssueCode.custom,

289

message: "Tax ID required for business",

290

path: ["taxId"],

291

});

292

}

293

});

294

295

// Complex validation with multiple issues

296

const ComplexSchema = z.object({ data: z.any() }).superRefine((val, ctx) => {

297

// Add multiple issues

298

// Access context path

299

// Conditional validation

300

// Fatal errors

301

});

302

303

// Combine with transformations

304

const ValidateAndTransform = z

305

.string()

306

.trim()

307

.refine((val) => val.length > 0)

308

.transform((val) => val.toUpperCase());

309

```

310

311

## Best Practices

312

313

```typescript

314

// Use refine for simple boolean checks

315

z.string().refine((val) => val.length > 0)

316

317

// Use superRefine for:

318

// - Multiple issues

319

// - Complex validation

320

// - Conditional validation

321

// - Access to context path

322

323

// Async refinements require parseAsync

324

const asyncSchema = z.string().refine(async (val) => {

325

return await checkAvailability(val);

326

});

327

await asyncSchema.parseAsync(data); // Required

328

329

// Error handling with refinements

330

const result = z

331

.number()

332

.refine((val) => val > 0, "Must be positive")

333

.safeParse(-1);

334

335

if (!result.success) {

336

console.log(result.error.issues[0].message); // "Must be positive"

337

}

338

```

339