colinhacks / zod

TypeScript-first schema validation with static type inference
https://zod.dev
MIT License
33.93k stars 1.18k forks source link

No way to merge multiple schemas. request function to merge deep multiple schemas like javascript object spread #2424

Closed markgarel17 closed 1 year ago

markgarel17 commented 1 year ago

v3.20.6

We got no way to merge the 3 or more schemas into 1 schema.

GiftVehicle.ts file

export const VehicleSchema = merge CarSchema, TruckSchema and SuperCarSchema 

Goal:
VehicleSchema = z.object({
  authToken: z.string(),
  colour:  z.string(),
  options: z
    .object({
      giftMessage: z.string().optional(),
      isConvertible: z.boolean().optional(),
      hasHugeWheels: z.boolean().optional(),
    })
    .optional(),
})

If we use merge, for more than 2 e.g. CarSchema.merge(TruckSchema).merge(SuperCarSchema)

it doesn't copy the deeper options correctly. after test, the TruckSchema.options.hasHugeWheels disappears.

union or discriminatedUnion is not making it due to the way it works. its not merging of object properties.

any suggestions? please help thank you...

. . . . .

GiftCar.ts file

export const CarSchema = z.object({
  authToken: z.string(),
  colour:  z.string(),
  options: z
    .object({
      giftMessage: z.string().optional(),
      isConvertible: z.boolean().optional(),
    })
    .optional(),
})

call giftCar API

GiftTruck.ts file

export const TruckSchema = z.object({
  authToken: z.string(),
  colour:  z.string(),
  options: z
    .object({
      giftMessage: z.string().optional(),
      hasHugeWheels: z.boolean().optional(),
    })
    .optional(),
})

call giftTruck API

GiftSuperCar.ts file

export const SuperCarSchema = z.object({
  authToken: z.string(),
  colour:  z.string(),
  options: z
    .object({
      giftMessage: z.string().optional(),
      isConvertible: z.boolean().optional(),
    })
    .optional(),
})

call giftSuperCar API
grcodemonkey commented 1 year ago

If you split out the options schemas then this is a much easier problem to solve.

There is no "discriminator" field for the union, so there's no way to programmatically determine the correct scenario for type validation.

I would expect to see something more like this:

const CarOptionsSchema = z.object({
  type: z.literal('car'),
  giftMessage: z.string().optional(),
  isConvertible: z.boolean().optional(),
})

const TruckOptionsSchema = z.object({
  type: z.literal('truck'),
  giftMessage: z.string().optional(),
  hasHugeWheels: z.boolean().optional(),
})

const SuperCarOptionsSchema = z.object({
  type: z.literal('superCar'),
  giftMessage: z.string().optional(),
  isConvertible: z.boolean().optional(),
})

const CarOptionTypesSchema = z.discriminatedUnion('type', [
  CarOptionsSchema, 
  TruckOptionsSchema, 
  SuperCarOptionsSchema
])

export const CarApiSchema = z.object({
  authToken: z.string(),
  colour: z.string(),
  options: CarOptionTypesSchema.optional(),
})
markgarel17 commented 1 year ago

@grcodemonkey Hey, thank you for the advise. this gave me insight and was able to do in another way.

I should've been more clearer with the title too. to request a function that can merge deep multiple schemas.

VehicleSchema = z.merge([ CarSchema, TruckSchema, SuperCarSchema ])

with the same result as javascript object, and spread operator.

const vehicle = {
...Car,
...Truck,
...SuperCar
}
stale[bot] commented 1 year ago

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.

sannajammeh commented 11 months ago

Requesting to reopen this.

lausek commented 11 months ago

Can't you just use intersection/schema.and(...)?

const VehicleSchema = TruckSchema.and(CarSchema).and(SuperCarSchema)
ptrxyz commented 8 months ago

Can't you just use intersection/schema.and(...)?

const VehicleSchema = TruckSchema.and(CarSchema).and(SuperCarSchema)

Depends on what you want to achieve:

const s1 = z.object({ id: z.string() })
const s2 = z.object({ id: z.number(), prop: z.string() })
const totalAnd = s1.and(s2)   // { id = never }
const totalOr = s1.or(s2) // { id = number | string; prop = string }

const merged = merge(s1, s2) // { id = number; prop = string }

So, question: how would a function merge look like, that adds props from s2 to s1, overwriting props in s1 if already present?

geoffreygarrett commented 7 months ago

One problem is that:

const VehicleSchema = TruckSchema.and(CarSchema).and(SuperCarSchema)

Results in a ZodIntersection, not a ZodObject, so I can't use .passthrough() on it.

Also:

So, question: how would a function merge look like, that adds props from s2 to s1, overwriting props in s1 if already present?

Yes, exactly like:

const merged = {
...s1,
...s2
}
geoffreygarrett commented 7 months ago

In fact, this works for me:

const VehicleSchema = z.object({
   ...TruckSchema.shape,
   ...SuperCarSchema.shape
});
StiliyanKushev commented 6 months ago

Why is this not supported yet?

chriskuech commented 5 months ago

I too would like this reopened please

StiliyanKushev commented 5 months ago

In fact, this works for me:

const VehicleSchema = z.object({
   ...TruckSchema.shape,
   ...SuperCarSchema.shape
});

This works in some cases but not In others. It fails to work where you have a more complex zod type such as when using .optional() for example.

nanotower commented 5 months ago

Will be useful, I agree. I was wondering how to do that and I got here.

chriskuech commented 5 months ago

It fails to work where you have a more complex zod type

Yes it fails for all the features that distinguish zod from its mediocre competitors 😛

ggrantrowberry commented 3 months ago

I would like to see this as well.