zod
zod copied to clipboard
intersection between `object()` and `record()` parsing fails
I have an intersection between a z.object() and a z.record() that has a refined key. Parsing an input fails since it seems to require all the input keys to match the record and the object, despite typescript seeing it as valid against the type.
version: 3.20.6
Example:
type FooKey = `foo.${string}`
const isValidFooKey = (key: string): key is FooKey => key.startsWith("foo.")
const Foo = z.record(
z.string().refine(isValidFooKey),
z.string(),
)
type Foo = z.infer<typeof Foo> // type Foo = { [x: `foo.${string}`]: string | undefined; }
console.log(Foo.parse({
"foo.x": "some string value",
})) // succeeds, output: `{ 'foo.x': 'some string value' }`
const Bar = z.object({
bar: z.string().optional(),
})
type Bar = z.infer<typeof Bar> // type Bar = { bar: string | undefined; }
console.log(Bar.parse({
bar: "another string value",
})) // succeeds, output: `{ bar: 'another string value' }`
const BarAndFoo = Bar.and(Foo)
type BarAndFoo = z.infer<typeof BarAndFoo> // type BarAndFoo = { bar: string | undefined; } & Partial<Record<`foo.${string}`, string>>
console.log(BarAndFoo.parse({
"foo.x": "some string value",
})) // succeeds, output: `{ 'foo.x': 'some string value' }`
console.log(BarAndFoo.parse({
bar: "another string value",
})) /* fails with: ```ZodError: [
{
"code": "custom",
"message": "Invalid input",
"path": [
"bar"
]
}
]``` */
console.log(BarAndFoo.parse({
"foo.x": "some string value",
bar: "another string value",
})) /* fails with: ```ZodError: [
{
"code": "custom",
"message": "Invalid input",
"path": [
"bar"
]
}
]``` */
const x: BarAndFoo = {
"foo.x": "some string value",
bar: "another string value",
} // typescript is happy with this
I also just ran into this issue. intersection ~~in combination with record~~ seems to be flawed. The following code shouldn't even be accepted by typescript, but more importantly it just fails no matter what I pass to it.
const first = z.record(
z.union([z.literal("one"), z.literal("two"), z.literal("three")]),
z.string(),
);
const second = z.record(
z.union([z.literal("ones"), z.literal("twos"), z.literal("threes")]),
z.string().array(),
);
const intersect = z.intersection(first, second);
console.log(intersect.parse({
one: ["example"],
}));
Just ran into the same issue with this type:
export interface Transform {
[schema: string]: {
[table: string]: boolean | RowTransform<RowShape>
}
}
const transformConfigOptionsSchema = z.object({
$mode: z
.union([z.literal('auto'), z.literal('strict'), z.literal('unsafe')])
.optional(),
$parseJson: z.boolean().optional(),
})
const transformConfigTableSchema = z.record(
z.string().describe('table'),
z.union([
z
.function()
.args(
z.object({ rowIndex: z.number(), row: z.record(z.string(), z.any()) })
)
.returns(z.record(z.string(), z.any())),
z.record(z.string(), z.any()),
])
)
export const transformConfigSchema = z.intersection(
transformConfigOptionsSchema,
z.record(
z
.string()
.refine((s) => s !== '$mode' && s !== '$parseJson')
.describe('schema'),
transformConfigTableSchema
)
)
export type TransformConfig = z.infer<typeof transformConfigSchema>
Expected to be able to parse this kind of object:
{
$mode: 'auto',
public: {
books: (ctx) => ({ title: 'A Long Story' }),
},
},
But it fails if I pass both the $mode and public: {...} parameters. However if I remove the $mode, then it's working fine.
Also the type from the schema when infered seems correct:
type TransformConfig = {
$mode?: "auto" | "strict" | "unsafe" | undefined;
$parseJson?: boolean | undefined;
} & Record<string, Record<string, Record<string, any> | ((args_0: {
rowIndex: number;
row: Record<string, any>;
}, ...args_1: unknown[]) => Record<...>)>>
Edit:
I managed a workaround by using a custom validator:
const optionsKeys = ['$mode', '$parseJson']
const customTransformConfigValidator = (transformConfig: unknown) => {
if (typeof transformConfig !== 'object' || !transformConfig) {
throw new ZodError([
{
code: 'invalid_type',
expected: 'object',
received: transformConfig as any,
path: ['transform'],
fatal: true,
message: 'Transform is not a valid objet',
},
])
}
for (const [key, value] of Object.entries(
transformConfig as Record<string, unknown>
)) {
// Validate special keys
if (optionKeys.includes(key)) {
transformConfigOptionsSchema.parse({ [key]: value })
} else {
transformConfigTableSchema.parse({ [key]: value })
}
}
return true
}
const customTransformConfigSchema = z.custom<
z.infer<
typeof transformConfigOptionsSchema | typeof transformConfigTableSchema
>
>(customTransformConfigValidator)
export type TransformConfig = z.infer<typeof customTransformConfigSchema>
Facing the same problem, the inferred type is correct but fails when parsing. For example:
const CommonKeysSchema = z.object({
order: z.string().optional(),
sortBy: z.string().optional(),
})
const MappingFileSchema = z.intersection(
CommonKeysSchema,
z.record(z.nativeEnum(ENUM), AnotherSchema)
)
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
It is still a problem in 3.22.2.
import {z} from 'zod';
type SetType = 'a' | 'b' | 'c';
type OrigType = { [key in SetType]?: number[] } & { disabled?: boolean };
const SetTypeSchema = z.enum(['a', 'b', 'c']);
const TypeSchema = z.intersection(
z.record(SetTypeSchema, z.array(z.number())),
z.object({
disabled: z.boolean().optional()
})
);
type Type = z.infer<typeof TypeSchema>;
const a: OrigType = {a: [1]};
const zA: Type = a; // ok
TypeSchema.parse(a); // ok
console.log('a OK');
const b: OrigType = {b: [1], disabled: true};
const zB: Type = b; // ok
TypeSchema.parse(b); // runtime error: `Invalid enum value. Expected 'a' | 'b' | 'c', received 'disabled'` and `Expected array, received boolean`
console.log('b OK');
Type in my case looks correct, but I am not sure if z.record should really be making all fields optional by default (Partial). Copied from IDE:
type Type = Partial<Record<"a" | "b" | "c", number[]>> & { disabled?: boolean | undefined; }
I'm having a similar issue with 3.22, basically I have an object with a uids key with is an array of string containing the ids of the entities which also are keys in the same object (yeah I agree that kinda sucks but that's the shape of an api I don't control). Basically, my desired type is:
type MyObject = { uids: string[] } & Record<string, { uid: string }>
I can create this schema:
const result = z.intersection(
z.record(
z.object({
uid: z.string(),
})
),
z.object({
uids: z.array(z.string()),
})
);
Which z.infer tells me is:
type Result = Record<string, {
uid: string;
}> & {
uids: string[];
}
And the result is inferred correctly in vscode:
But when you run the code, the parsing fails because it tries to parse both types for all keys:
ZodError: [
{
"code": "invalid_type",
"expected": "object",
"received": "array",
"path": [
"result",
"uids"
],
"message": "Expected object, received array"
}
]
As of right now I haven't found a workaround.
Ok, so not sure if this helps anyone but I have been painfully trying to deal with this type of error myself
"message": "Expected object, received array"
which stems from using the z.record() approach above on my schema to help include some dynamic keys.
My example comes from using React-Hook-Form and trying to connect my dynamic input to the dynamic schema key. For some reason, if I give it a small tag before the string() key it can correctly infer the type and everything pulls in correctly.
eg. When registering my RHF input field that is deeply nested
const deeplyNestedSchema = z.object({ first: z.string(), year_amounts: z.record(z.string(), z.coerce.number()) })
{...register(first.second.${index}.year_amounts.${dynamicKey}} this doesnt work...
{...register(first.second.${index}.year_amounts.tag_${dynamicKey}} this does work...
Not fully sure why this works and the other approach causes so much pain but its helped move my project along. For context, had tried intersection / .and / .transform etc