zod
zod copied to clipboard
ZodObject that are `.refine` or `.superRefine` returns a ZodEffects type
Zod version 3.21.4
Problem
Using .refine()
or .superRefine()
on ZodObject
(z.object()
) prevents the use of .shape
or .merge()
method as the returned type is a ZodEffects
and not a ZodObject
.
Example
const schema1 = z.object({ foo: z.string() }) // = type ZodObject
const schema2 = z.object({
bar: z.string(),
attributeRelatedToBar: z.string(),
}).superRefine(handleSchema2Errors) // = type ZodEffects
/**
* Impossible because `.merge` expect `schema2` to be type of `ZodObject` instead of `ZodEffects`:
* TS2352: Conversion of type 'ZodEffects […] to type 'ZodObject ' may be a mistake because
* neither type sufficiently overlaps with the other.
* If this was intentional, convert the expression to 'unknown' first.
*/
const finalSchema = schema1.merge(schema2)
/**
* Same error with `.shape` that expect a `ZodObject` instead of `ZodEffects`:
* TS2339: Property 'shape' does not exist on type 'ZodEffects'.
*/
schema2.shape.bar
schema2.shape.attributeRelatedToBar
Expected behavior
Using .superRefine()
or .refine()
on z.object()
should return a ZodObject
type
– or –
.merge()
& .shape
should be functional on ZodEffects
applied to a ZodObject
.
You should use .and
see https://github.com/colinhacks/zod/issues/1147
You should use
.and
see #1147
@thibaultleouay
Thank you. But I don't see .and
as a solution but as a workaround 😄.
Using .superRefine() or .refine() on z.object() should return a ZodObject type
This is the expected behavior. The unexpected behavior needs to be documented.
I don't see how .and
is a replacement for calling refine or superRefine on a whole object to be able to access the properties in validation logic.
Using .superRefine() or .refine() on z.object() should return a ZodObject type
This is the expected behavior. The unexpected behavior needs to be documented.
I don't see how
.and
is a replacement for calling refine or superRefine on a whole object to be able to access the properties in validation logic.
I solved this by adding a wrapper object around the actual object to validate. This way it is possible to use refine on nested objects while returning a type compatible with ZodTypeAny.
const schema2 = z.object({ wrapper: z.object({ bar: z.string(), attributeRelatedToBar: z.string(), }).superRefine(handleSchema2Errors) }).
Then add a wrapper middleware to wrap the post data before you parse it and add some custom unwrapper to get the proper form field errors.
It also prevents the use of discrimatedUnion
.
Is this what you are looking for?
const schema1 = z.object( { foo: z.string() } )
const schema2 = z.object( {
bar: z.string(),
attributeRelatedToBar: z.string(),
} )
const finalSchema = schema1.merge( schema2 ).superRefine( handleSchema2Errors )
schema2.shape.bar // z.ZodString
schema2.shape.attributeRelatedToBar // z.ZodString
If you found my answer satisfactory, please consider supporting me. Even a small amount is greatly appreciated. Thanks friend! 🙏 https://github.com/sponsors/JacobWeisenburger
Is this what you are looking for?If you found my answer satisfactory, please consider supporting me. Even a small amount is greatly appreciated. Thanks friend! 🙏[Code]
const schema1 = z.object( { foo: z.string() } ) const schema2 = z.object( { bar: z.string(), attributeRelatedToBar: z.string(), } ) const finalSchema = schema1.merge( schema2 ).superRefine( handleSchema2Errors ) schema2.shape.bar // z.ZodString schema2.shape.attributeRelatedToBar // z.ZodString
https://github.com/sponsors/JacobWeisenburger
The schema is a validation schema and must be fully functional. So in my context, this is a necessity to have a refine
|superRefine
to make the validation work.
There is a problem between ZodObject
and ZodEffects
type that needs to be fixed.
I don't want to use a workaround.
Even if I wanted to use a workaround, the solution you suggested would have been easy to find.
The schema is a validation schema and must be fully functional.
How is what I gave you not "fully functional"?
So in my context, this is a necessity to have a
refine
|superRefine
to make the validation work.
I used superRefine
in my solution. ✅
There is a problem between
ZodObject
andZodEffects
type that needs to be fixed.
I fail to see the problem that you are talking about. Please explain more.
I don't want to use a workaround.
Considering Zod doesn't release very often, your choices are to use a workaround in the mean time or wait for your problem to be addressed and then wait for it to be released. Based on how things have been going, that could be 6 months. If that's ok for you, then great.
Even if I wanted to use a workaround, the solution you suggested would have been easy to find.
Generally when someone tries to help you, it's not the best approach to belittle their efforts. But thanks.
How is what I gave you not "fully functional"?
I am working on a dynamic form that can change depending on the persona. So I've created a schema by input, which is then assembled according to the displayed input.
Some inputs have special validation and need a refine
attached to a schema to be more reusable and easily importable.
In this case, if I have to do what you suggest for each schema, it doesn't make the code more readable or maintainable and can be prone to forgetting.
I used
superRefine
in my solution. ✅
Yes, but at the end of the final schema and if I need to merge multiple object, what the method refine receive in parameter will change according to what it's merged inside. Not easy to maintain.
I fail to see the problem that you are talking about. Please explain more.
To be more concrete, say I have a WYSIWYG input and I need to count how many characters are written (without taking into account html tags). To do that, I need to use refine
to count plain text instead of generated html.
On top of that, I have multiple inputs that need special validation using refine/superfine according to persona and some user configuration.
Considering Zod doesn't release very often, your choices are to use a workaround in the mean time or wait for your problem to be addressed and then wait for it to be released. Based on how things have been going, that could be 6 months. If that's ok for you, then great.
I have another workaround on the validation side after a .safeParse()
. I'm not happy with it, but it's the least dirty I can do for now.
Generally when someone tries to help you, it's not the best approach to belittle their efforts. But thanks.
Sorry, I didn't mean to be mean, but from the way I described the bug, you can imagine that I'm not a junior. The proposed solution wasn't very complex and the reason for this issue was a real problem with the library. I just want to show this problem so that a solution can be found.
Seems that .omit()
isn't allowed after a .refine()
, which is not useful.
@JacobWeisenburger
function validationFunction<SchemaA extends z.ZodTypeAny, SchemaB extends z.ZodTypeAny>(schemas: MySchemaParamType<SchemaA, SchemaB>): SomethingThatUsesTheTypeInfo<SchemaA, SchemaB> { }
This will break when you pass it a schema that uses refine or superRefine, the resulting type ZodEffects does not extend ZodTypeAny (that constraint is required).
Up on this topic ? Issue still the same with zod ^3.22.4
This unfortunately make complex types into a real pain to use with this library
I came across as well. How to fix this? By fix I mean that I want to keep the chaining of the schema, like merging another schema or do anything else.
If refine shouldn't mean to support this, what does so?
I am running into the same issue but after using .transform
. My use case is as follows:
const conversionFn = (val) => val === null ? undefined : val;
const parentSchema = z.object({
// Accepts `number | null | undefined` but returns `number | undefined`
numberProp: z.number().nullable().optional().transform<number | undefined>(conversionFn),
});
const childSchema = parentSchema.required({ numberProp: true });
Because my parentSchema.numberProp
is set to a ZodEffect
the .required
does not apply to it since ZodEffect
does not extend ZodOptional
or ZodNullable
.
It is also a similar problem when using .pipe
which returns a ZodPipeline
.
I'm running into issues as well. It doesn't make sense to me for the object to transform itself into a different type if refine
is chained to it.
const testSchema = z.object({
foo: z.string()
})
const fooSchema = testSchema.shape.foo // works!
const testSchema2 = z.object({
foo: z.string()
}).refine(x => true)
const fooSchema2 = testSchema2.shape.foo // does not work
What not just apply your .refine + .transform
at the end of all validations/merges/etc.? That's what we do.
@oyatek It breaks the composition
Example 1
Let's imagine that we have a reusable type utility:
// utils/reusableSchema.ts
export const activeDateTimes = z
.object({
activeFromDateTime: z.string().datetime().optional(),
activeToDateTime: z.string().datetime().optional(),
})
.superRefine((data: ActiveDateTimes, ctx) => {
if (
data.activeFromDateTime &&
data.activeToDateTime &&
new Date(data.activeFromDateTime) > new Date(data.activeToDateTime)
) {
ctx.addIssue({
path: ['activeToDateTime'],
code: z.ZodIssueCode.custom,
message: 'Active to date should be after active from date',
});
ctx.addIssue({
path: ['activeFromDateTime'],
code: z.ZodIssueCode.custom,
message: 'Active from date should be before active to date',
});
}
});
And you want to reuse it in multiple places:
const userSchema = z.object({name: z.string}).merge(activeDateTimes)
const postSchema = z.object({title: z.string}).merge(activeDateTimes)
...
The current approach makes it difficult to implement a schema utility and, in some ways, breaks schema encapsulation.
Example 2
You have some schema that represent API endpoint
const user = z.object({
id: z.string(),
name: z.string(),
age: z.string()
}).refine(...)
But for the API POST method, you want to modify your schema and omit id
prop
const createUserSchema = user.omit({id: true});
You may have noticed that there is an issue with the current implementation of your application. Specifically, you may need to refine some of the schemas during the latest stage of development. However, the proposed solution would require you to rewrite a large portion of your application.
Same issue here! I have pretty large object schemas that need to be changed altogether, and I am trying to use functions similar to the following for that.
export const ignoreDateTypeValidation = <Schema extends z.AnyZodObject>(schema: Schema) => {
const entries = Object.entries(schema.shape) as [keyof Schema['shape'], z.ZodTypeAny][]
const newProps = entries.reduce(
(acc, [key, value]) => {
acc[key] = value instanceof z.ZodDate ? z.any() : value
return acc
},
{} as { [key in keyof Schema['shape']]: Schema['shape'][key] },
)
return z.object(newProps)
}
the issue is that the date validation does't get picked up because it is using refine()
export const pastDateSchema = z.date().refine((d) => d.getTime() <= Date.now(), { message: 'cannot use future dates' })
but I cannot use z.ZodEffects
because then everything that isn't a date()
validation is also going to get triggered.
acc[key] = value instanceof z.ZodEffects ? z.any() : value
The same goes if I use and()
, since the outcome is that everything becomes an intersection instead of an effect.
Is there any way for me to pick up the validation type even after using refine? I am fine with any workarounds for now
Same issue here - I'm trying to add/replace a part of a schema to avoid duplicating the code dozens of times:
export const specSchema = z.object({
value: z.string(),
project: projectSchema,
});
export const globalSchema = z.preprocess(
(val: any) => {
const { kind, name, displayName, ...other }: { [key: string]: any } = val;
return {
kind,
metadata: { name, annotations: { displayName } },
spec: other,
};
},
z.object({
kind: z.string().optional(),
metadata: metadataSchema,
spec: specSchema,
})
);
but I can't use extend or merge to do that...
(I tried handing in the specSchema via function parameter, ending up with a valid schema, that is missing the type details about spec
.)
This isn't only an issue for the object schema. It is an issue for all schemas.
.refine()
and .superRefine()
should return a schema that has all the same properties as what was there before it was used.
Making these changes to the repo fixes the core issue https://github.com/colinhacks/zod/compare/master...Dan503:zod:2474-refine-not-returning-correct-types-info
I haven't made a PR because it still has type conflict errors and is failing a lot of unit tests.
Has anyone managed to resolve this? I have this problem, I use .refine and now I want to use .pick but it doesn't exist in ZodEffect.
I've stumbled upon this as well...
I create a super refiner programmatically, so I don't really have access to the base schema:
export function passwordsMatchRefiner<Augmentation extends typeof this.passwordTwins>(
schema: Augmentation
): z.ZodEffects<Augmentation> {
return schema.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "error.passwords_dont_match"
});
}
export const customerSignUpFieldsSchema = passwordsMatchRefiner(
someSchema.merge(someOtherSchema)
);
what about this? (I just discovered innerType
)
const myPickedSchema = customerSignUpFieldsSchema.innerType().pick({
actionCode: !0
});
Does it suck? It that intended to be used this way?
@KikoCosmetics innerType() seems to remove the refine logic, at least for me it is no longer running. So no, this is not a solution.
This will be changed in Zod 4 — refinements will be stored in the schema itself instead of in an enclosing ZodEffects
.
@KikoCosmetics innerType() seems to remove the refine logic, at least for me it is no longer running. So no, this is not a solution.
In my scenario it is a solution, cause I only picked a field that is not refined.
But yes if I had to pick the passwords I should refine them again. Not very cool, but that’s essentially the reason why I created that ‘passwordMatchRefiner’ function…