Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove usage of instanceof for Zod schemas #601

Merged
merged 1 commit into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,7 @@ node_modules/
/test-results/
/playwright-report/
/playwright/.cache/

# DS Store
.DS_Store
**/.DS_Store
126 changes: 61 additions & 65 deletions packages/conform-zod/coercion.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
import {
type ZodType,
type ZodTypeAny,
type output,
ZodString,
ZodEnum,
ZodLiteral,
ZodNumber,
ZodBoolean,
ZodDate,
ZodArray,
ZodBigInt,
ZodNativeEnum,
ZodObject,
ZodLazy,
ZodIntersection,
ZodUnion,
ZodDiscriminatedUnion,
ZodTuple,
ZodPipeline,
ZodEffects,
ZodAny,
ZodNullable,
ZodOptional,
ZodDefault,
lazy,
any,
ZodCatch,
} from 'zod';
import type {
ZodDiscriminatedUnionOption,
ZodFirstPartySchemaTypes,
ZodType,
ZodTypeAny,
output,
} from 'zod';

/**
* Helpers for coercing string value
Expand Down Expand Up @@ -80,7 +74,7 @@ export function isFileSchema(schema: ZodEffects<any, any, any>): boolean {

return (
schema._def.effect.type === 'refinement' &&
schema.innerType() instanceof ZodAny &&
schema.innerType()._def.typeName === 'ZodAny' &&
schema.safeParse(new File([], '')).success &&
!schema.safeParse('').success
);
Expand Down Expand Up @@ -110,31 +104,32 @@ export function enableTypeCoercion<Schema extends ZodTypeAny>(
}

let schema: ZodTypeAny = type;
let def = (type as ZodFirstPartySchemaTypes)._def;

if (
type instanceof ZodString ||
type instanceof ZodLiteral ||
type instanceof ZodEnum ||
type instanceof ZodNativeEnum
def.typeName === 'ZodString' ||
def.typeName === 'ZodLiteral' ||
def.typeName === 'ZodEnum' ||
def.typeName === 'ZodNativeEnum'
) {
schema = any()
.transform((value) => coerceString(value))
.pipe(type);
} else if (type instanceof ZodNumber) {
} else if (def.typeName === 'ZodNumber') {
schema = any()
.transform((value) =>
coerceString(value, (text) =>
text.trim() === '' ? Number.NaN : Number(text),
),
)
.pipe(type);
} else if (type instanceof ZodBoolean) {
} else if (def.typeName === 'ZodBoolean') {
schema = any()
.transform((value) =>
coerceString(value, (text) => (text === 'on' ? true : text)),
)
.pipe(type);
} else if (type instanceof ZodDate) {
} else if (def.typeName === 'ZodDate') {
schema = any()
.transform((value) =>
coerceString(value, (timestamp) => {
Expand All @@ -151,11 +146,11 @@ export function enableTypeCoercion<Schema extends ZodTypeAny>(
}),
)
.pipe(type);
} else if (type instanceof ZodBigInt) {
} else if (def.typeName === 'ZodBigInt') {
schema = any()
.transform((value) => coerceString(value, BigInt))
.pipe(type);
} else if (type instanceof ZodArray) {
} else if (def.typeName === 'ZodArray') {
schema = any()
.transform((value) => {
// No preprocess needed if the value is already an array
Expand All @@ -175,102 +170,103 @@ export function enableTypeCoercion<Schema extends ZodTypeAny>(
})
.pipe(
new ZodArray({
...type._def,
type: enableTypeCoercion(type.element, cache),
...def,
type: enableTypeCoercion(def.type, cache),
}),
);
} else if (type instanceof ZodObject) {
} else if (def.typeName === 'ZodObject') {
const shape = Object.fromEntries(
Object.entries(type.shape).map(([key, def]) => [
Object.entries(def.shape()).map(([key, def]) => [
key,
// @ts-expect-error see message above
enableTypeCoercion(def, cache),
]),
);
schema = new ZodObject({
...type._def,
...def,
shape: () => shape,
});
} else if (type instanceof ZodEffects) {
if (isFileSchema(type)) {
} else if (def.typeName === 'ZodEffects') {
if (isFileSchema(type as unknown as ZodEffects<any, any, any>)) {
schema = any()
.transform((value) => coerceFile(value))
.pipe(type);
} else {
schema = new ZodEffects({
...type._def,
schema: enableTypeCoercion(type.innerType(), cache),
...def,
schema: enableTypeCoercion(def.schema, cache),
});
}
} else if (type instanceof ZodOptional) {
} else if (def.typeName === 'ZodOptional') {
schema = any()
.transform((value) => coerceFile(coerceString(value)))
.pipe(
new ZodOptional({
...type._def,
innerType: enableTypeCoercion(type.unwrap(), cache),
...def,
innerType: enableTypeCoercion(def.innerType, cache),
}),
);
} else if (type instanceof ZodDefault) {
} else if (def.typeName === 'ZodDefault') {
schema = any()
.transform((value) => coerceFile(coerceString(value)))
.pipe(
new ZodDefault({
...type._def,
innerType: enableTypeCoercion(type.removeDefault(), cache),
...def,
innerType: enableTypeCoercion(def.innerType, cache),
}),
);
} else if (type instanceof ZodCatch) {
} else if (def.typeName === 'ZodCatch') {
schema = new ZodCatch({
...type._def,
innerType: enableTypeCoercion(type.removeCatch(), cache),
...def,
innerType: enableTypeCoercion(def.innerType, cache),
});
} else if (type instanceof ZodIntersection) {
} else if (def.typeName === 'ZodIntersection') {
schema = new ZodIntersection({
...type._def,
left: enableTypeCoercion(type._def.left, cache),
right: enableTypeCoercion(type._def.right, cache),
...def,
left: enableTypeCoercion(def.left, cache),
right: enableTypeCoercion(def.right, cache),
});
} else if (type instanceof ZodUnion) {
} else if (def.typeName === 'ZodUnion') {
schema = new ZodUnion({
...type._def,
options: type.options.map((option: ZodTypeAny) =>
...def,
options: def.options.map((option: ZodTypeAny) =>
enableTypeCoercion(option, cache),
),
});
} else if (type instanceof ZodDiscriminatedUnion) {
} else if (def.typeName === 'ZodDiscriminatedUnion') {
schema = new ZodDiscriminatedUnion({
...type._def,
options: type.options.map((option: ZodTypeAny) =>
...def,
options: def.options.map((option: ZodTypeAny) =>
enableTypeCoercion(option, cache),
),
optionsMap: new Map(
Array.from(type.optionsMap.entries()).map(([discriminator, option]) => [
Array.from(def.optionsMap.entries()).map(([discriminator, option]) => [
discriminator,
enableTypeCoercion(option, cache),
enableTypeCoercion(option, cache) as ZodDiscriminatedUnionOption<any>,
]),
),
});
} else if (type instanceof ZodTuple) {
} else if (def.typeName === 'ZodTuple') {
schema = new ZodTuple({
...type._def,
items: type.items.map((item: ZodTypeAny) =>
...def,
items: def.items.map((item: ZodTypeAny) =>
enableTypeCoercion(item, cache),
),
});
} else if (type instanceof ZodNullable) {
} else if (def.typeName === 'ZodNullable') {
schema = new ZodNullable({
...type._def,
innerType: enableTypeCoercion(type.unwrap(), cache),
...def,
innerType: enableTypeCoercion(def.innerType, cache),
});
} else if (type instanceof ZodPipeline) {
} else if (def.typeName === 'ZodPipeline') {
schema = new ZodPipeline({
...type._def,
in: enableTypeCoercion(type._def.in, cache),
out: enableTypeCoercion(type._def.out, cache),
...def,
in: enableTypeCoercion(def.in, cache),
out: enableTypeCoercion(def.out, cache),
});
} else if (type instanceof ZodLazy) {
schema = lazy(() => enableTypeCoercion(type.schema, cache));
} else if (def.typeName === 'ZodLazy') {
const inner = def.getter();
schema = lazy(() => enableTypeCoercion(inner, cache));
}

if (type !== schema) {
Expand Down
95 changes: 42 additions & 53 deletions packages/conform-zod/constraint.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
import type { Constraint } from '@conform-to/dom';
import {
type ZodTypeAny,
ZodArray,
ZodDefault,
ZodDiscriminatedUnion,
ZodEffects,
ZodEnum,
ZodIntersection,

import type {
ZodTypeAny,
ZodFirstPartySchemaTypes,
ZodNumber,
ZodObject,
ZodOptional,
ZodPipeline,
ZodString,
ZodUnion,
ZodTuple,
ZodLazy,
} from 'zod';

const keys: Array<keyof Constraint> = [
Expand All @@ -37,35 +27,32 @@ export function getZodConstraint(
name = '',
): void {
const constraint = name !== '' ? (data[name] ??= { required: true }) : {};
const def = (schema as ZodFirstPartySchemaTypes)['_def'];

if (schema instanceof ZodObject) {
for (const key in schema.shape) {
updateConstraint(
schema.shape[key],
data,
name ? `${name}.${key}` : key,
);
if (def.typeName === 'ZodObject') {
for (const key in def.shape()) {
updateConstraint(def.shape()[key], data, name ? `${name}.${key}` : key);
}
} else if (schema instanceof ZodEffects) {
updateConstraint(schema.innerType(), data, name);
} else if (schema instanceof ZodPipeline) {
} else if (def.typeName === 'ZodEffects') {
updateConstraint(def.schema, data, name);
} else if (def.typeName === 'ZodPipeline') {
// FIXME: What to do with .pipe()?
updateConstraint(schema._def.out, data, name);
} else if (schema instanceof ZodIntersection) {
updateConstraint(def.out, data, name);
} else if (def.typeName === 'ZodIntersection') {
const leftResult: Record<string, Constraint> = {};
const rightResult: Record<string, Constraint> = {};

updateConstraint(schema._def.left, leftResult, name);
updateConstraint(schema._def.right, rightResult, name);
updateConstraint(def.left, leftResult, name);
updateConstraint(def.right, rightResult, name);

Object.assign(data, leftResult, rightResult);
} else if (
schema instanceof ZodUnion ||
schema instanceof ZodDiscriminatedUnion
def.typeName === 'ZodUnion' ||
def.typeName === 'ZodDiscriminatedUnion'
) {
Object.assign(
data,
(schema.options as ZodTypeAny[])
(def.options as ZodTypeAny[])
.map((option) => {
const result: Record<string, Constraint> = {};

Expand Down Expand Up @@ -111,41 +98,43 @@ export function getZodConstraint(
} else if (name === '') {
// All the cases below are not allowed on root
throw new Error('Unsupported schema');
} else if (schema instanceof ZodArray) {
} else if (def.typeName === 'ZodArray') {
constraint.multiple = true;
updateConstraint(schema.element, data, `${name}[]`);
} else if (schema instanceof ZodString) {
if (schema.minLength !== null) {
constraint.minLength = schema.minLength;
updateConstraint(def.type, data, `${name}[]`);
} else if (def.typeName === 'ZodString') {
let _schema = schema as ZodString;
if (_schema.minLength !== null) {
constraint.minLength = _schema.minLength ?? undefined;
}
if (schema.maxLength !== null) {
constraint.maxLength = schema.maxLength;
if (_schema.maxLength !== null) {
constraint.maxLength = _schema.maxLength;
}
} else if (schema instanceof ZodOptional) {
} else if (def.typeName === 'ZodOptional') {
constraint.required = false;
updateConstraint(schema.unwrap(), data, name);
} else if (schema instanceof ZodDefault) {
updateConstraint(def.innerType, data, name);
} else if (def.typeName === 'ZodDefault') {
constraint.required = false;
updateConstraint(schema.removeDefault(), data, name);
} else if (schema instanceof ZodNumber) {
if (schema.minValue !== null) {
constraint.min = schema.minValue;
updateConstraint(def.innerType, data, name);
} else if (def.typeName === 'ZodNumber') {
let _schema = schema as ZodNumber;
if (_schema.minValue !== null) {
constraint.min = _schema.minValue;
}
if (schema.maxValue !== null) {
constraint.max = schema.maxValue;
if (_schema.maxValue !== null) {
constraint.max = _schema.maxValue;
}
} else if (schema instanceof ZodEnum) {
constraint.pattern = schema.options
} else if (def.typeName === 'ZodEnum') {
constraint.pattern = def.values
.map((option: string) =>
// To escape unsafe characters on regex
option.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d'),
)
.join('|');
} else if (schema instanceof ZodTuple) {
for (let i = 0; i < schema.items.length; i++) {
updateConstraint(schema.items[i], data, `${name}[${i}]`);
} else if (def.typeName === 'ZodTuple') {
for (let i = 0; i < def.items.length; i++) {
updateConstraint(def.items[i], data, `${name}[${i}]`);
}
} else if (schema instanceof ZodLazy) {
} else if (def.typeName === 'ZodLazy') {
// FIXME: If you are interested in this, feel free to create a PR
}
}
Expand Down
Loading