---
title: Use Coercion for Form and Query Data
impact: CRITICAL
impactDescription: Form data and query params are always strings; without coercion, z.number() rejects "42" and z.boolean() rejects "true"
tags: schema, coerce, forms, query-params
---

## Use Coercion for Form and Query Data

HTML forms and URL query parameters always transmit data as strings. Using `z.number()` on form data will fail because `"42"` is not a number. Use `z.coerce.number()` to automatically convert strings to the correct type.

**Incorrect (no coercion for form data):**

```typescript
import { z } from "zod";

const searchSchema = z.object({
  query: z.string(),
  page: z.number(), // Expects actual number
  limit: z.number(),
  showDeleted: z.boolean(), // Expects actual boolean
});

// Form data / query params are strings
const formData = new URLSearchParams("query=test&page=1&limit=10&showDeleted=true");
const params = Object.fromEntries(formData);
// { query: 'test', page: '1', limit: '10', showDeleted: 'true' }

searchSchema.parse(params);
// ZodError: Expected number, received string at "page"
// ZodError: Expected number, received string at "limit"
// ZodError: Expected boolean, received string at "showDeleted"
```

**Correct (using coercion):**

```typescript
import { z } from "zod";

const searchSchema = z.object({
  query: z.string(),
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(10),
  showDeleted: z.coerce.boolean().default(false),
});

// Form data / query params are strings
const formData = new URLSearchParams("query=test&page=1&limit=10&showDeleted=true");
const params = Object.fromEntries(formData);

const result = searchSchema.parse(params);
// { query: 'test', page: 1, limit: 10, showDeleted: true }
// Types are correct: number, number, boolean
```

**Available coercion types:**

```typescript
z.coerce.string(); // Converts anything to string via String(value)
z.coerce.number(); // Converts via Number(value), NaN fails validation
z.coerce.boolean(); // Truthy/falsy conversion
z.coerce.bigint(); // Converts via BigInt(value)
z.coerce.date(); // Converts via new Date(value)
```

**Coercion edge cases:**

```typescript
// z.coerce.number() behavior
z.coerce.number().parse("42"); // 42
z.coerce.number().parse(""); // 0 (empty string becomes 0!)
z.coerce.number().parse("abc"); // ZodError (NaN fails)

// z.coerce.boolean() behavior
z.coerce.boolean().parse("true"); // true
z.coerce.boolean().parse("false"); // true! (non-empty string is truthy)
z.coerce.boolean().parse(""); // false
z.coerce.boolean().parse("0"); // true! (non-empty string)

// For strict boolean parsing from strings:
const strictBooleanSchema = z.enum(["true", "false"]).transform(v => v === "true");
```

**When NOT to use this pattern:**

- When receiving JSON payloads (already typed correctly)
- When you want strict type checking without conversion

Reference: [Zod API - Coercion](https://zod.dev/api#coercion)
