You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何构建Zod Schema:基于已有字段动态设置必填项

问题背景

有如下TypeScript接口:

interface Purchase {
  invoiceNo?: string
  purchaseDate?: string
  placeOfPurchase?: string
  productId: string
  serialId?: string
  purchasePrice?: number
}

编辑后端记录时,要求已存在值的字段必须设为必填,不存在值的字段可忽略。比如某记录包含productId、serialId、purchaseDate三个字段,校验时需强制这三个字段的类型与必填性,其余字段不做要求。当前实现繁琐,需要更简洁、可复用的解决方案。


优化方案1:基于Zod原生能力动态生成Schema

核心利用Zod的partial()和required()方法,结合实体数据动态生成校验规则,类型安全且贴合Zod设计理念。

步骤1:定义基础Schema

先维护所有字段的基础校验规则,原本必填的productId保持必填,其余字段先设为可选:

import { z } from "zod";

const basePurchaseSchema = z.object({
  invoiceNo: z.string().trim().min(1, { message: 'Invoice No is required' }).optional(),
  purchaseDate: z.string().trim().min(1, { message: 'Purchase Date is required' }).optional(),
  placeOfPurchase: z.string().trim().min(1, { message: 'Place of Purchase is required' }).optional(),
  productId: z.string().trim().min(1, { message: 'Product is required' }),
  serialId: z.string().trim().min(1, { message: 'Serial Number is required' }).optional(),
  purchasePrice: z.number().min(1, { message: 'Purchase Price is required' }).optional(),
}).strict();

步骤2:编写通用动态Schema生成函数

一个函数搞定字段筛选和Schema生成,支持复用在其他实体场景:

import type { ZodObject, ZodRawShape } from "zod";

function getDynamicRequiredSchema<Schema extends ZodObject<ZodRawShape>>(
  baseSchema: Schema,
  entity: Partial<z.infer<Schema>>
) {
  // 提取实体中存在有效值的字段(排除undefined/null)
  const existingKeys = Object.entries(entity)
    .filter(([_, value]) => value !== undefined && value !== null)
    .map(([key]) => key) as Array<keyof z.infer<Schema>>;

  // 先将所有字段设为可选,再把存在值的字段和原本必填的字段设为必填
  return baseSchema.partial().required({
    ...Object.fromEntries(existingKeys.map(key => [key, true])),
    productId: true,
  });
}

步骤3:结合react-hook-form使用

直接传入动态生成的Schema,默认值无需额外处理:

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

// 示例:从后端获取的现有记录
const purchase: Purchase = {
  productId: "PROD-123",
  serialId: "SER-456",
  purchaseDate: "2024-05-20"
};

const dynamicSchema = getDynamicRequiredSchema(basePurchaseSchema, purchase);

const { register, handleSubmit, formState: { errors } } = useForm({
  resolver: zodResolver(dynamicSchema),
  defaultValues: purchase, // 直接传入实体,react-hook-form会自动忽略不存在的字段
});

优化方案2:精简现有工具函数

如果不想重构基础Schema,可以合并原有两个工具函数的逻辑,减少冗余:

import { z } from "zod";
import type { ZodObject } from "zod";
import { get } from "lodash";

// 合并Schema筛选和默认值生成逻辑
function getActiveFormConfig<S extends ZodObject<any>, E>(schema: S, entity: E) {
  const activeKeys = Object.keys(schema.shape).filter(key => get(entity, key));
  return {
    activeSchema: schema.pick(Object.fromEntries(activeKeys.map(k => [k, true]))),
    defaultValues: Object.fromEntries(activeKeys.map(k => [k, get(entity, k)])) as Partial<E>
  };
}

// 使用方式
const { activeSchema, defaultValues } = getActiveFormConfig(schema, purchase);
const { register, handleSubmit, formState: { errors } } = useForm({
  resolver: zodResolver(activeSchema),
  defaultValues,
});

方案优势

  • 方案1:类型安全性更强,无需手动维护字段映射,新增字段只需在basePurchaseSchema中添加,复用性更高。
  • 方案2:改动小,适合基于现有代码快速优化,减少重复逻辑。

内容的提问来源于stack exchange,提问作者Hard-Boiled Wonderland

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 05:46:00