V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
FlashEcho
V2EX  ›  程序员

如何优雅地使用 zod

  •  
  •   FlashEcho · 1 天前 · 1515 次点击

    使用 zod 的时候,下面几个很难受的地方:

    • 数据库的 orm 框架用的是 drizzle ,这个时候得先用 drizzle 定义数据库 schema ,没法优先使用 zod 定义 schema ,导出成 drizzle 数据库表

    • 由于在数据库端 drizzle 优先了,在后端服务端有两种选择:

      1. 直接用 drizzle-zod 导出的 insert, select 等 schema ,后端服务直接用这个导出的 schema ,这样最方便,但是这个 schema 无形中变宽松了,有大量 optional ,而且如果有多种 insert 情况,就失去了严格的约束
      2. 再在后端手写一套 zod schema ,但是这样就会有大量重复和冗余,而且程序员需要自己手动维护 zod schema 和数据库 schema 的同步。这实际上是一个逻辑或者说业务上的实体,但是没法从一个唯一来源只写一次导出到处用,在服务和数据库上重复写了两次,也很难受

    请问各位大佬是怎么处理这种问题的

    4 条回复    2026-01-07 15:58:53 +08:00
    huijiewei
        1
    huijiewei  
       1 天前   ❤️ 1
    没什么冗余的说法

    持久层的 model 和服务层的 model 还是建议区分开.哪怕重复也要分开
    Ketteiron
        2
    Ketteiron  
       1 天前   ❤️ 3
    以 drizzle 的数据库 schema 为第一优先级,在此之上派生下一级的用于接口校验、表单验证的 schema 。
    现代化的 typescript 工程,要严格遵守 DRY ,不允许在任何地方出现多次重复定义,分离数据库层与业务层实际上与复用逻辑并不冲突,没有写两遍的必要。
    无论何时,应该只有一个唯一来源,应用于整个项目,包括前端以及所有中间件服务,并强制保证运行时类型与 ts 类型完全等同。

    基于这个理论,不应该直接使用 createInsertSchema 和 createUpdateSchema ,因为它们也是在重复定义并不明确的类型,应该封装一个方法统一给所有下一级 schema 使用。

    它可能长这样 const createSchema = (table,labels)=> createInsertSchema(table).pick(labels)
    具体代码不贴了,有一些类型体操,需要保证输入限制以及 pick 生效

    举个例子,我定义好一张表
    export const userTable = pgTable('user', {
    ...baseTable,
    username: varchar({ length: 32 }).notNull().unique(),
    password: varchar({ length: 64 }).notNull(),
    })
    然后这么使用
    export const userLoginSchema = createSchema(userTable, {
    username: (s) => s.min(4),
    password: (s) => s.min(6),
    })
    因为返回的是一个 zod 对象,可以按照业务逻辑进行 extend 不在数据库中的其他参数,或者根据业务进行覆盖

    第一参数接收一个数据库表,以此限制第二参数输入的键名,value 是一个函数,参数是经过 drizzle-zod 生成后的 schema
    drizzle-zod 会为 varchar({ length: 32 }) 这样的定义自动生成 z.string().max(32),但是它不会限制最小长度,因此派生的 schema 需要在原来基础上进行补充,或者也可以直接传入一个 zod 对象覆盖。

    不应该对业务层暴露一大堆的 optional ,而是严格限定你只能传递什么东西过来,它要么必须有值要么禁止传递,从数据库层一层一层往下传递到所有涉及到的地方,一直到达前端表单,这才是 schema("契约")的正确用法之一。

    还有个地方需要注意,这个 schema 不能直接在 monorepo 中被前端/中间件引入,因为包含完整的数据库模式以及用不到的大量运行时函数,直接 import 大概会打包出两百多 k ,需要使用 json-to-zod + zod-to-json(zod4 最新版已内置) 等变通方法,监听 schema 文件自动生成一个新文件并 export 给其他项目。当然因为无法序列化函数,所以 refine 等功能都用不了。

    此外我对 createSchema 进行了大量改造,包括强制传递元数据,并在路由/表单通过包装 safeParseAsync 函数解析出适合人类阅读的提示信息:'用户名长度最低为 4 位'、'密码长度最低 6 位'、'缺少 xxx 字段'、'xxx 格式错误',其中'用户名'和'密码'就是我要求传递的提示文本,它同样只能定义一次,还可以进行 i18n 改造,所有其它信息可以通过遍历 code 、origin 、input 等进行填充,社区里大量使用的 zod i18n 库是一个不理想的变通方法,但图省事也可以去用。
    FlashEcho
        3
    FlashEcho  
    OP
       1 天前
    @Ketteiron #2 谢谢大佬,看来还是我对于 drizzle-zod 的使用不够深入,不然应该可以少在在 api 处定义很多重复的 schema

    希望什么时候 drizzle-zod 支持先用 zod 定义 schema 再转成数据库 schema 就好了,zod 的表现能力强,可以削弱一层给 drizzle 用,也能原封不动给 api 处用,天然好做成单一事实来源 。drizzle 表现能力弱,先定义好数据库 schema ,再加一层约束给 api 处用就很难让普通用户一下自然领会最佳实践
    wings110
        4
    wings110  
       15 小时 26 分钟前
    @Ketteiron 谢谢佬!解决了我一直以来的困惑!!
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   1095 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 23:25 · PVG 07:25 · LAX 15:25 · JFK 18:25
    ♥ Do have faith in what you're doing.