zod: 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.

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 53
  • Comments: 24

Most upvoted comments

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

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

It also prevents the use of discrimatedUnion.

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.

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

You should use .and see #1147

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

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

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

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.

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

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.

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?

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.

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.