colinhacks / zod

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

How to union or merge two or more z.record schemas #3745

Open asuthr-zesty opened 2 weeks ago

asuthr-zesty commented 2 weeks ago

Thanks for the great library, really finding it useful.

Is there a recommended way to define a union of two or more record schemas, that would successfully parse an object with a mix of key/values from each record? Here's a minimal example outlining my unsuccessful attempt:

import { z } from 'zod';

// Define the schemas for the individual records
const record1Schema = z.record(z.enum(['a', 'b']), z.union([z.number(), z.string()]));

const record2Schema = z.record(z.enum(['c', 'd']), z.union([z.boolean(), z.string()]));

// Define the merged schema
const mergedSchema = z.union([record1Schema, record2Schema]);

// Example usage
const example1 = { a: 42, b: 'hello' }; // Valid according to record1Schema
const example2 = { c: true, d: 'world' }; // Valid according to record2Schema
const example3 = { a: 42, c: true }; // Valid according to mergedSchema

console.log(record1Schema.safeParse(example1)); // Success
console.log(record2Schema.safeParse(example2)); // Success
console.log(mergedSchema.safeParse(example3)); // Error

The last parse fails with invalid_union errors that includes invalid_enun_values errors like this:

{                                                                                                                
              "received": "c",               
              "code": "invalid_enum_value",      
              "options": [                                                                                                   
                "a",            
                "b"                                                                                                          
              ],                                                                                                             
              "path": [                                                                                                      
                "c"                                                                                                          
              ],                                                                                                             
              "message": "Invalid enum value. Expected 'a' | 'b', received 'c'"                   
            }

I would expect it to work (and it does type check), but it seems like it expects it to conform to one of the record schemas only, and won't accept the other. Is there a better way to do this?

sunnylost commented 16 hours ago

I suggest using superRefine() to gain more custom control.