zod icon indicating copy to clipboard operation
zod copied to clipboard

ZodObject that are `.refine` or `.superRefine` returns a ZodEffects type

Open Morgan-jarry opened this issue 1 year ago • 27 comments

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.

Morgan-jarry avatar Jun 01 '23 15:06 Morgan-jarry

You should use .and see https://github.com/colinhacks/zod/issues/1147

thibaultleouay avatar Jun 14 '23 15:06 thibaultleouay

You should use .and see #1147

@thibaultleouay Thank you. But I don't see .and as a solution but as a workaround 😄.

Morgan-jarry avatar Jun 16 '23 14:06 Morgan-jarry

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.

matjaeck avatar Aug 10 '23 15:08 matjaeck

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.

matjaeck avatar Aug 11 '23 13:08 matjaeck

It also prevents the use of discrimatedUnion.

MidasVE avatar Aug 24 '23 14:08 MidasVE

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

JacobWeisenburger avatar Sep 26 '23 17:09 JacobWeisenburger

Is this what you are looking for?
[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
If you found my answer satisfactory, please consider supporting me. Even a small amount is greatly appreciated. Thanks friend! 🙏

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.

Morgan-jarry avatar Sep 28 '23 15:09 Morgan-jarry

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 and ZodEffects 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.

JacobWeisenburger avatar Sep 28 '23 15:09 JacobWeisenburger

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.

Morgan-jarry avatar Sep 28 '23 16:09 Morgan-jarry

Seems that .omit() isn't allowed after a .refine(), which is not useful.

aryzing avatar Sep 29 '23 20:09 aryzing

@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).

matjaeck avatar Oct 02 '23 13:10 matjaeck

Up on this topic ? Issue still the same with zod ^3.22.4

Goldiggy avatar Jan 09 '24 10:01 Goldiggy

This unfortunately make complex types into a real pain to use with this library

henriksjostrom avatar Jan 15 '24 06:01 henriksjostrom

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?

razb-viola avatar Jan 21 '24 06:01 razb-viola

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.

trey-yeager-FHR avatar Jan 22 '24 17:01 trey-yeager-FHR

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

robokozo avatar Feb 22 '24 17:02 robokozo

What not just apply your .refine + .transform at the end of all validations/merges/etc.? That's what we do.

oyatek avatar Mar 04 '24 03:03 oyatek

@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.

mykhailo-ivankiv avatar Mar 11 '24 19:03 mykhailo-ivankiv

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

TheBlindHawk avatar Mar 18 '24 05:03 TheBlindHawk

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.)

max06 avatar Mar 23 '24 15:03 max06

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.

Dan503 avatar Apr 03 '24 10:04 Dan503

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.

Dan503 avatar Apr 03 '24 12:04 Dan503

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.

mabitten avatar Apr 05 '24 17:04 mabitten

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 avatar Apr 27 '24 18:04 KikoCosmetics

@KikoCosmetics innerType() seems to remove the refine logic, at least for me it is no longer running. So no, this is not a solution.

Eric-Arz avatar May 08 '24 15:05 Eric-Arz

This will be changed in Zod 4 — refinements will be stored in the schema itself instead of in an enclosing ZodEffects.

colinhacks avatar May 08 '24 17:05 colinhacks

@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…

KikoCosmetics avatar May 08 '24 21:05 KikoCosmetics