javascript - วิธีการจัดการ `discriminatedUnion` ด้วย `superRefine` กับ Zod
โครงร่างการตรวจสอบแบบไดนามิก
สคีมาเบื้องต้น: สร้างสคีมาเบื้องต้นที่มีฟิลด์ทั่วไป
การตรวจสอบแบบไดนามิก:
- รหัส: นำการตรวจสอบแบบไดนามิกไปใช้งานตามฟิลด์
code - แฟลก: ใช้แฟลกที่ส่งมาเป็นพารามิเตอร์เพื่อตัดสินใจว่าควรใช้การตรวจสอบเพิ่มเติมหรือไม่
- การตรวจสอบความถูกต้องที่ซับซ้อนร่วมกัน: ใช้
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 แต่คงจะดูแฮ็กเกินไปสำหรับวิธีแก้ปัญหาที่แฮ็กอยู่แล้ว
