ค้นหาเว็บไซต์

javascript - วิธีการจัดการ `discriminatedUnion` ด้วย `superRefine` กับ Zod


โครงร่างการตรวจสอบแบบไดนามิก

  1. สคีมาเบื้องต้น: สร้างสคีมาเบื้องต้นที่มีฟิลด์ทั่วไป

  2. การตรวจสอบแบบไดนามิก:

  • รหัส: นำการตรวจสอบแบบไดนามิกไปใช้งานตามฟิลด์ code
  • แฟลก: ใช้แฟลกที่ส่งมาเป็นพารามิเตอร์เพื่อตัดสินใจว่าควรใช้การตรวจสอบเพิ่มเติมหรือไม่
  1. การตรวจสอบความถูกต้องที่ซับซ้อนร่วมกัน: ใช้ superRefine ในสคีมาหลักเพื่อรักษาตรรกะการตรวจสอบความถูกต้องที่ใช้ร่วมกันในสคีมาทุกแบบ

ตัวอย่างโค้ด

type Codes = {
    One: 'one',
    Two: 'two',
}

const BaseSchema = z.object({
    code,
    // ... other common fields
}).superRefine((data, ctx) => {
    // Common complex validation logic
});

const One = z.object({
  email,
});
// ... more specific schemas like `Two`

const FlaggedSchema = z.object({
  dateOfBirth,
});

const createSchema = (code: Codes, isFlagged: boolean) => {
    const isOne = code === Codes.One
    const baseSchema = BaseSchema.extend({
        code: z.literal(code),
        ...(isOne ? One.shape : {}),
        ...(isFlagged ? FlaggedSchema.shape : {}),
    });

    return baseSchema;
};

export const createMapSchema = (isFlagged: boolean) => {
  const schemas = Object.values(Codes).map((code) =>
    createSchema(code, isFlagged),
  );
  const unions = z.discriminatedUnion('code', schemas);
  const map = z.record(z.array(unions));
  const schema = z.object({
    types: map,
  });
  return schema;
};

ปัญหา

superRefine ไม่สามารถใช้กับสกีมาหลักได้ถ้าคุณวางแผนที่จะใช้เมธอดของสกีมาเช่น merge หลังจากนั้น ข้อจำกัดนี้เกิดขึ้นเพราะ superRefine คืนค่า Zod effect ซึ่งทำให้ไม่สามารถแก้ไขสกีมาเพิ่มเติมได้

ดังนั้น วิธีการใช้การตรวจสอบความถูกต้องที่ซับซ้อนทั่วไป (refine หรือ superRefine) ในสกีมาเบส แต่ยังสามารถรันการตรวจสอบความถูกต้องแบบไดนามิกตามฟิลด์ของสกีมา หรือข้อมูลภายนอกได้อย่างไร?

หมายเหตุเพิ่มเติม

ฉันได้ตรวจสอบคำถามนี้แล้วแต่ฉันไม่เห็นอะไรที่จะใช้กับกรณีเฉพาะนี้

คำตอบ #1

อินสแตนซ์ของคลาส ZodEffects (รวมถึงทายาท ZodType อื่นๆ) จะมีคุณสมบัติ _def ที่ประกอบด้วยข้อมูลทั้งหมดที่จำเป็นในการสร้างวัตถุขึ้นใหม่จากส่วนประกอบของมัน ดูโค้ดต้นฉบับ types.ts#L89.

วัตถุ ZodEffects zodEffects ที่ถูกสร้างขึ้นโดย .refine หรือ .superRefine จะมี zodEffects._def.schema ซึ่งชี้ไปยังสำเนาของ ZodType ต้นฉบับที่มีการใช้ .refine หรือ .superRefine และ zodEffects._def.effect.refinement ที่ประกอบด้วยอาร์กิวเมนต์ของวิธีการปรับปรุง (refinement).

ฟังก์ชันต่อไปนี้ (ทดลอง) applyToRefined ใช้ method เช่น extend กับ zodType ที่สามารถเป็นได้ทั้ง ZodObject หรือ ZodEffects ซึ่งได้มาจากการปรับปรุง ZodObject

มันตามห่วงโซ่ของ _def จนกว่าจะพบอ็อบเจ็กต์ที่ไม่ถูกปรับแต่ง แล้วใช้วิธีการกับอ็อบเจ็กต์นั้น (เช่น extend) และจากนั้นจึงนำการปรับแต่งกลับมาใช้ใหม่:

function applyToRefined(zodType: z.ZodType, method: string, ...args: any[]): z.ZodType{
    const refinements: any[] = [];
    while(zodType instanceof z.ZodEffects){
        if(zodType._def?.effect?.type !== 'refinement'){
            throw new Error('Cannot handle object that is not produced by .refine or .superRefine');
        }
        refinements.push(zodType._def.effect.refinement);
        zodType = zodType._def.schema;
    }

    if(!zodType[method] || !zodType[method].apply){
        throw new Error(`The schema of the ZodEffects object doesn't have the method ${method}`);
    }
    zodType = zodType[method].apply(zodType, args);
    while(refinements.length > 0){
        const refinement = refinements.shift();
        zodType = zodType.superRefine(refinement);
    }
    return zodType;
}

กรณีทดสอบ ซึ่งอิงจากตัวอย่างจากเอกสาร (เนื่องจากตัวอย่างในคำถามนั้นไม่ครบถ้วนพอที่ฉันจะใช้ แต่ขอบเขตควรเหมือนกัน):

const BaseSchema = z.object({
        first: z.string(),
        second: z.number(),
    })
    .superRefine((arg: any, ctx: any) => {
        if(Math.round(arg.second) !== arg.second){
            ctx.addIssue({
                code: z.ZodIssueCode.invalid_type,
                message: ".second should be integer",
            });
        }
    })
    .refine((arg: any) => arg.first.length > 0, {
        message: ".first should not be the empty string"
    });

function dynamicExtend(zodType: z.ZodType, code: string, otherStringProp: string){
    return applyToRefined(zodType, 'extend', {
        code: z.literal(code),
        [otherStringProp]: z.string()
    });
}

const BaseSchema1 = dynamicExtend(BaseSchema, 'one', 'andOne');
const BaseSchema2 = dynamicExtend(BaseSchema, 'two', 'andTwo');

console.log(BaseSchema1.parse({first: 'one', second: 100, code: 'one', andOne: '1'}));
console.log(BaseSchema2.parse({first: 'one', second: 100, code: 'two', andTwo: '2'}));
try{
    console.log(BaseSchema1.parse({first: '', second: 100.1, code: 'one', andOne: '2'}));
}
catch(err){
    console.error(err);
}
try{
    console.log(BaseSchema1.parse({first: '', second: 100.1, code: 'two'}));
    //see https://github.com/colinhacks/zod/discussions/2971#discussioncomment-7588935
}
catch(err){
    console.error(err);
}

เพื่อที่จะสร้าง union ที่แยกประเภทของ ZodEffect เราสามารถใช้กลยุทธ์เดียวกันกับใน applyToRefined แต่กับอาเรย์ของ ZodEffects:

function combineRefined(
    zodTypes: z.ZodType[],
    combinatorFunction: (...args: z.ZodType[]) => z.ZodType
): z.ZodType {
    const refinements: any[] = [];
    const zodSchemas: z.ZodType[] = [];
    let zodType: z.ZodType;
    for(zodType of zodTypes){
        while(zodType instanceof z.ZodEffects){
            if(zodType._def?.effect?.type !== 'refinement'){
                throw new Error('Cannot handle object that is not produced by .refine or .superRefine');
            }
            refinements.push(zodType._def.effect.refinement);
            zodType = zodType._def.schema;
        }
        zodSchemas.push(zodType);
    }
    zodType = combinatorFunction(...zodSchemas);
    while(refinements.length > 0){
        const refinement = refinements.shift();
        zodType = zodType.superRefine(refinement);
    }
    return zodType;
}


// create the union of the BaseSchema1, BaseSchema2 above, discriminated by "code" property
const BaseSchema12 = combineRefined([BaseSchema1, BaseSchema2],
    (...schemas: z.ZodType[]) => z.discriminatedUnion("code", schemas));

และการทดสอบสคีมาด้วย .parse:

console.log('--------------------');
console.log(BaseSchema12.parse({code: 'one', first: 'one', second: 100, andOne: '1'}));
console.log(BaseSchema12.parse({code: 'two', first: 'one', second: 100, andTwo: '2'}));
try{
    console.log(BaseSchema12.parse({code: 'one', first: 'one', second: 100, andTwo: '1'}));
    // error: no andOne
}
catch(err){
    console.error(err);
}

try{
    console.log(BaseSchema12.parse({code: 'one', first: '', second: 100.1, andOne: '1'}));
    // refine errors for first and second
}
catch(err){
    console.error(err);
}

ไฟล์ไทพ์สคริปต์ทั้งหมดในสแตกบลิทซ์ (ผลลัพธ์ในคอนโซลเบราว์เซอร์)

สามารถพิจารณาเพื่อความสะดวกในการเพิ่มเมธอดเช่น extend หรือ discriminatedUnion โดยตรงไปยังคลาส ZodEffects แต่คงจะดูแฮ็กเกินไปสำหรับวิธีแก้ปัญหาที่แฮ็กอยู่แล้ว