Skip to content

Commit 6197f1a

Browse files
edmundhungchimame
andauthored
feat(conform-zod): add disableAutoCoercion option with coerceFormValue helper (#871)
Co-authored-by: chimame <rito.tamata@gmail.com>
1 parent 19c4cff commit 6197f1a

File tree

10 files changed

+1375
-871
lines changed

10 files changed

+1375
-871
lines changed

.changeset/hot-geese-run.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@conform-to/zod': minor
3+
---
4+
5+
feat(conform-zod): add `disableAutoCoercion` option with `coerceFormValue` helper

docs/api/zod/coerceFormValue.md

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# unstable_coerceFormValue
2+
3+
A helper that enhances the schema with extra preprocessing steps to strip empty value and coerce form value to the expected type.
4+
5+
```ts
6+
const enhancedSchema = coerceFormValue(schema, options);
7+
```
8+
9+
The following rules will be applied by default:
10+
11+
1. If the value is an empty string / file, pass `undefined` to the schema
12+
2. If the schema is `z.number()`, trim the value and cast it with the `Number` constructor
13+
3. If the schema is `z.boolean()`, treat the value as `true` if it equals to `on` (Browser default `value` of a checkbox / radio button)
14+
4. If the schema is `z.date()`, cast the value with the `Date` constructor
15+
5. If the schema is `z.bigint()`, trim the value and cast it with the `BigInt` constructor
16+
17+
## Parameters
18+
19+
### `schema`
20+
21+
The zod schema to be enhanced.
22+
23+
### `options.defaultCoercion`
24+
25+
Optional. Set it if you want to [override the default behavior](#override-default-behavior).
26+
27+
### `options.defineCoercion`
28+
29+
Optional. Use it to [define custom coercion](#define-custom-coercion) for a specific schema.
30+
31+
## Example
32+
33+
```ts
34+
import { parseWithZod, unstable_coerceFormValue as coerceFormValue } from '@conform-to/zod';
35+
import { useForm } from '@conform-to/react';
36+
import { z } from 'zod';
37+
import { jsonSchema } from './jsonSchema';
38+
39+
const schema = coerceFormValue(
40+
z.object({
41+
ref: z.string()
42+
date: z.date(),
43+
amount: z.number(),
44+
confirm: z.boolean(),
45+
}),
46+
);
47+
48+
function Example() {
49+
const [form, fields] = useForm({
50+
onValidate({ formData }) {
51+
return parseWithZod(formData, {
52+
schema,
53+
defaultTypeCoercion: false,
54+
});
55+
},
56+
});
57+
58+
// ...
59+
}
60+
```
61+
62+
## Tips
63+
64+
### Override default behavior
65+
66+
You can override the default coercion by specifying the `defaultCoercion` mapping in the options.
67+
68+
```ts
69+
const schema = coerceFormValue(
70+
z.object({
71+
// ...
72+
}),
73+
{
74+
defaultCoercion: {
75+
// Trim the value for all string-based fields
76+
// e.g. `z.string()`, `z.number()` or `z.boolean()`
77+
string: (value) => {
78+
if (typeof value !== 'string') {
79+
return value;
80+
}
81+
82+
const result = value.trim();
83+
84+
// Treat it as `undefined` if the value is empty
85+
if (result === '') {
86+
return undefined;
87+
}
88+
89+
return result;
90+
},
91+
92+
// Override the default coercion with `z.number()`
93+
number: (value) => {
94+
// Pass the value as is if it's not a string
95+
if (typeof value !== 'string') {
96+
return value;
97+
}
98+
99+
// Trim and remove commas before casting it to number
100+
return Number(value.trim().replace(/,/g, ''));
101+
},
102+
103+
// Disable coercion for `z.boolean()`
104+
boolean: false,
105+
},
106+
},
107+
);
108+
```
109+
110+
### Default values
111+
112+
`coerceFormValue` will always strip empty values to `undefined`. If you need a default value, use `.transform()` to define a fallback value that will be returned instead.
113+
114+
```ts
115+
const schema = z.object({
116+
foo: z.string().optional(), // string | undefined
117+
bar: z
118+
.string()
119+
.optional()
120+
.transform((value) => value ?? ''), // string
121+
baz: z
122+
.string()
123+
.optional()
124+
.transform((value) => value ?? null), // string | null
125+
});
126+
```
127+
128+
### Define custom coercion
129+
130+
You can customize coercion for a specific schema by setting the `customize` option.
131+
132+
```ts
133+
import {
134+
parseWithZod,
135+
unstable_coerceFormValue as coerceFormValue,
136+
} from '@conform-to/zod';
137+
import { useForm } from '@conform-to/react';
138+
import { z } from 'zod';
139+
import { json } from './schema';
140+
141+
const metadata = z.object({
142+
number: z.number(),
143+
confirmed: z.boolean(),
144+
});
145+
146+
const schema = coerceFormValue(
147+
z.object({
148+
ref: z.string(),
149+
metadata,
150+
}),
151+
{
152+
customize(type) {
153+
// Customize how the `metadata` field value is coerced
154+
if (type === metadata) {
155+
return (value) => {
156+
if (typeof value !== 'string') {
157+
return value;
158+
}
159+
160+
// Parse the value as JSON
161+
return JSON.parse(value);
162+
};
163+
}
164+
165+
// Return `null` to keep the default behavior
166+
return null;
167+
},
168+
},
169+
);
170+
```

docs/api/zod/parseWithZod.md

Lines changed: 31 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,26 @@ const submission = parseWithZod(payload, options);
1212

1313
It could be either the **FormData** or **URLSearchParams** object depending on how the form is submitted.
1414

15-
### `options`
16-
17-
#### `schema`
15+
### `options.schema`
1816

1917
Either a zod schema or a function that returns a zod schema.
2018

21-
#### `async`
19+
### `options.async`
2220

2321
Set it to **true** if you want to parse the form data with **safeParseAsync** method from the zod schema instead of **safeParse**.
2422

25-
#### `errorMap`
23+
### `options.errorMap`
2624

2725
A zod [error map](https://github.com/colinhacks/zod/blob/master/ERROR_HANDLING.md#contextual-error-map) to be used when parsing the form data.
2826

29-
#### `formatError`
27+
### `options.formatError`
3028

3129
A function that let you customize the error structure and include additional metadata as needed.
3230

31+
### `options.disableAutoCoercion`
32+
33+
Set it to **true** if you want to disable [automatic type coercion](#automatic-type-coercion) and manage how the form data is parsed yourself.
34+
3335
## Example
3436

3537
```tsx
@@ -57,47 +59,40 @@ function Example() {
5759

5860
### Automatic type coercion
5961

60-
Conform will strip empty value and coerce the form data to the expected type by introspecting the schema and inject an extra preprocessing step. The following rules will be applied:
62+
By default, `parseWithZod` will strip empty value and coerce form value to the correct type by introspecting the schema and inject extra preprocessing steps using the [coerceFormValue](./coerceFormValue) helper internally.
6163

62-
1. If the value is an empty string / file, pass `undefined` to the schema
63-
2. If the schema is `z.string()`, pass the value as is
64-
3. If the schema is `z.number()`, trim the value and cast it with the `Number` constructor
65-
4. If the schema is `z.boolean()`, treat the value as `true` if it equals to `on`
66-
5. If the schema is `z.date()`, cast the value with the `Date` constructor
67-
6. If the schema is `z.bigint()`, cast the value with the `BigInt` constructor
68-
69-
You can override this behavior by setting up your own `z.preprocess` step in the schema.
70-
71-
> Note: There are several bug reports on Zod's repository regarding the behaviour of `z.preprocess` since v3.22, like https://github.com/colinhacks/zod/issues/2671 and https://github.com/colinhacks/zod/issues/2677. If you are experiencing any issues, please downgrade to v3.21.4.
64+
If you want to customize this behavior, you can disable automatic type coercion by setting `options.disableAutoCoercion` to `true` and manage it yourself.
7265

7366
```tsx
67+
import { parseWithZod } from '@conform-to/zod';
68+
import { useForm } from '@conform-to/react';
69+
import { z } from 'zod';
70+
7471
const schema = z.object({
72+
// Strip empty value and coerce the number yourself
7573
amount: z.preprocess((value) => {
76-
// If no value is provided, return `undefined`
77-
if (!value) {
74+
if (typeof value !== 'string') {
75+
return value;
76+
}
77+
78+
if (value === '') {
7879
return undefined;
7980
}
8081

81-
// Clear the formatting and cast the value to number
8282
return Number(value.trim().replace(/,/g, ''));
8383
}, z.number()),
8484
});
85-
```
8685

87-
### Default values
88-
89-
Conform will always strip empty values to `undefined`. If you need a default value, please use `.transform()` to define a fallback value that will be returned instead.
86+
function Example() {
87+
const [form, fields] = useForm({
88+
onValidate({ formData }) {
89+
return parseWithZod(formData, {
90+
schema,
91+
disableAutoCoercion: true,
92+
});
93+
},
94+
});
9095

91-
```tsx
92-
const schema = z.object({
93-
foo: z.string().optional(), // string | undefined
94-
bar: z
95-
.string()
96-
.optional()
97-
.transform((value) => value ?? ''), // string
98-
baz: z
99-
.string()
100-
.optional()
101-
.transform((value) => value ?? null), // string | null
102-
});
96+
// ...
97+
}
10398
```

0 commit comments

Comments
 (0)