Skip to content

Commit

Permalink
fix(conform-zod): empty string default value support (#741)
Browse files Browse the repository at this point in the history
  • Loading branch information
edmundhung authored Sep 10, 2024
1 parent a0e422f commit 3a7f02e
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 14 deletions.
9 changes: 9 additions & 0 deletions .changeset/empty-guests-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@conform-to/zod': patch
---

fix(conform-zod): empty string default value support

Previously, we suggested using `.default()` to set a fallback value. However, `.default()` does not work as expected with `z.string().default('')`. This issue has now been resolved, but keep in mind that the default value is still subject to validation errors. For more predictable results, we recommend using `.transform(value => value ?? defaultValue)` instead.

Fix #676
16 changes: 10 additions & 6 deletions docs/api/zod/parseWithZod.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,18 @@ const schema = z.object({

### Default values

Conform already preprocesses empty values to `undefined`. Add `.default()` to your schema to define a default value that will be returned instead.

Zod will return the default value if the input is `undefined` after preprocessing. This also has the effect of changing the schema return type.
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.

```tsx
const schema = z.object({
foo: z.string(), // string | undefined
bar: z.string().default('bar'), // string
baz: z.string().nullable().default(null), // string | null
foo: z.string().optional(), // string | undefined
bar: z
.string()
.optional()
.transform((value) => value ?? ''), // string
baz: z
.string()
.optional()
.transform((value) => value ?? null), // string | null
});
```
16 changes: 10 additions & 6 deletions docs/ja/api/zod/parseWithZod.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,18 @@ const schema = z.object({

### デフォルト値

Conform はすでに空の値を `undefined` に前処理しています。 `.default()` をスキーマに追加して、代わりに返されるデフォルト値を定義します。

Zod は、前処理後の入力が `undefined` の場合、デフォルト値を返します。これはスキーマの戻り値の型を変更する効果もあります。
Conform は常に空の文字列を削除し、それらを「undefined」にします。 `.transform()` をスキーマに追加して、代わりに返されるデフォルト値を定義します。

```tsx
const schema = z.object({
foo: z.string(), // string | undefined
bar: z.string().default('bar'), // string
baz: z.string().nullable().default(null), // string | null
foo: z.string().optional(), // string | undefined
bar: z
.string()
.optional()
.transform((value) => value ?? ''), // string
baz: z
.string()
.optional()
.transform((value) => value ?? null), // string | null
});
```
23 changes: 21 additions & 2 deletions packages/conform-zod/coercion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ import type {
output,
} from 'zod';

/**
* A special string value to represent empty string
* Used to prevent empty string from being stripped to undefined when using `.default()`
*/
const EMPTY_STRING = '__EMPTY_STRING__';

/**
* Helpers for coercing string value
* Modify the value only if it's a string, otherwise return the value as-is
Expand All @@ -38,6 +44,10 @@ export function coerceString(
return undefined;
}

if (value === EMPTY_STRING) {
return '';
}

if (typeof transform !== 'function') {
return value;
}
Expand Down Expand Up @@ -81,15 +91,15 @@ export function isFileSchema(schema: ZodEffects<any, any, any>): boolean {
}

/**
* @deprecated Conform coerce empty strings to undefined by default
* @deprecated Conform strip empty string to undefined by default
*/
export function ifNonEmptyString(fn: (text: string) => unknown) {
return (value: unknown) => coerceString(value, fn);
}

/**
* Reconstruct the provided schema with additional preprocessing steps
* This coerce empty values to undefined and transform strings to the correct type
* This strips empty values to undefined and coerces string to the correct type
*/
export function enableTypeCoercion<Schema extends ZodTypeAny>(
type: Schema,
Expand Down Expand Up @@ -212,6 +222,15 @@ export function enableTypeCoercion<Schema extends ZodTypeAny>(
.pipe(
new ZodDefault({
...def,
defaultValue: () => {
const value = def.defaultValue();

if (value === '') {
return EMPTY_STRING;
}

return value;
},
innerType: enableTypeCoercion(def.innerType, cache),
}),
);
Expand Down
32 changes: 32 additions & 0 deletions tests/conform-zod.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,8 @@ describe('conform-zod', () => {
d: z.date().default(defaultDate),
e: z.instanceof(File).default(defaultFile),
f: z.array(z.string()).default(['foo', 'bar']),
g: z.string().nullable().default(null),
h: z.string().default(''),
});
const emptyFile = new File([], '');

Expand Down Expand Up @@ -878,6 +880,36 @@ describe('conform-zod', () => {
d: defaultDate,
e: defaultFile,
f: ['foo', 'bar'],
g: null,
h: '',
},
reply: expect.any(Function),
});

const today = new Date();
const schema2 = z.object({
a: z.string().email('invalid').default(''),
b: z.number().gt(10, 'invalid').default(0),
c: z
.boolean()
.refine((value) => !!value, 'invalid')
.default(false),
d: z.date().min(today, 'invalid').default(defaultDate),
e: z
.instanceof(File)
.refine((file) => file.size > 100, 'invalid')
.default(defaultFile),
});

expect(parseWithZod(createFormData([]), { schema: schema2 })).toEqual({
status: 'error',
payload: {},
error: {
a: ['invalid'],
b: ['invalid'],
c: ['invalid'],
d: ['invalid'],
e: ['invalid'],
},
reply: expect.any(Function),
});
Expand Down

0 comments on commit 3a7f02e

Please sign in to comment.