Jakarta Validation and Zod

Date:February 1, 2023Tags:jakarta, validation, zod

Jakarta Validation and Zod

javax/jakarta Validation

对 Java 同学来说,javax Validation 或 Jakarta Validation 并不陌生,你只要在 Java Bean 上加上一些注解,就可以对 Java Bean 进行校验,样例代码如下:

public class User {
    @Positive
    private Long id;
    private UUID uuid;
    @NotNull
    @Size(max = 120)
    private String name;
    @Email
    private String email;
    @Pattern(regexp = "(https?)://example\\.com/avatars/[a-f0-9]{10,20}")
    private String avatar;
    @Pattern(regexp = "[a-f0-9]{40,120}")
    private String passwordHash;
    @NotNull
    @Size(min = 8, max = 8)
    private String salt;
    @Pattern(regexp = "\\+?[0-9]{8,16}")
    private String phone;
    @NotNull
    @PastOrPresent
    private Date createdAt;
    // ...
}

接下来就是对 User 实例进行校验,也是非常简单,只要使用Validator.validate()进行验证即可,最后对返回值进行判断即可,如果没有错误,表示校验通过,否则校验失败。

  final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
  Set<ConstraintViolation<User>> constraintViolations = validator.validate(user);
  System.out.println(constraintViolations);

Zod Schema Validation

回到 TypeScript,如果也需要对数据对象进行验证,你可以采用类似的机制,目前来说可能就是非常受欢迎的Zod 库了。

Zod 的使用方式也非常简单,但是还是有些不同。和定义 class 不同,你需要顶一个定义一个 Schema,样例代码如下:

import { z } from "zod";

const UserSchema = z.object({
  id: z.number(),
  uuid: z.string().uuid(),
  name: z.string().max(120),
  email: z.string().email(),
  avatar: z.string().url().max(120).optional(),
  passwordHash: z.string().min(40).max(128),
  salt: z.string().min(8).max(8),
  phone: z.string().regex(/^(\+?)\d{8,16}$/),
  createdAt: z.date(),
});

type User = z.infer<typeof UserSchema>;

这里要注意一下,UserSchema并不是真正的 TypeScript 类型,你需要通过z.infer来获取真正的类型,这里的User就是真正的 TS 类型。 你可以将 Schema 理解为元数据(metadata),然后从元数据生成具体的数据类型。

有了 Schema,你就可以对数据进行校验了,只要调用 Schema 的parse()方法即可,如果校验失败,会抛出异常,你可以捕获异常,然后进行处理。

try {
  UserSchema.parse(user);
} catch (error) {
  //
}

如果你不喜欢这种 try-catch 方式,还可以使用UserSchema.safeParse(),这个方法会返回一个ParseResult对象,你可以通过success属性来判断是否校验成功。

数据验证的使用场景

TypeScript 和 Java 不太一样,并不能在运行期间获取到类型信息,这个也是为何我们要使用 Zod 定义 Schema 的原因,但是背后的出发点都是一致的,通过 Annotation 或者 Schema 这些元数据来完成数据的校验。

除了能够校验数据以外,Jakarta Validation 和 Zod 同时还可以提供测试数据自动生成,如 Java 你可以使用Easy Random/Faker JUnit 5 extension 就可以完成对象数据的自动生成,而 Zod 则可以使用zod-mock来完成对象数据的自动生成。

Jakarta Validation 作为 Java 的规范已经被各种框架所支持,如 Spring Boot,Hibernate Validator,Quarkus 等。Zod 虽然是一个独立的库,但是鉴于其优势,越来越多的框架开始支持 Zod, 最典型的就是tRPC,它是一个基于 Zod 的 API 框架。最新的 Astro 2.0 也使用 Zod 定义 Content 的 Schema,方便开发者更安全地使用 Content。

总结

数据验证一直都是开发中不可缺的一个环节,无论是在前端还是后端,都需要对数据进行校验,以确保数据的正确性。Jakarta Validation 和 Zod 都是非常优秀的数据验证库,它们都提供了非常丰富的功能,简化了开发者的工作。

如果你要问这两者,哪个功能更强大,无疑就是 Zod,可以说强大地超过你想象。Zod 非常灵活,可以设定更多的自定义校验规则,transform, fallback, generic 等支持,当然 Java Validator 框架也可以做,但是相对来说还是不如 Zod 更加灵活。 写一个 Zod 介绍连载完全没有问题,目前我在做 TypeScript + DDD + Zod 的一些实践,后续会分享出来。

不少同学可能觉得 Zod 这种通过 Schema,然后推导出 Type 类型,看起来好像有点不太习惯,从代码量来说,Class + Annotation 和 Schema 定义,差别几乎没有,但是考虑到 Zod 提供的众多功能,这些写法还是值得的, 这也是 Zod 的优势所在,也是 Zod 越来越受欢迎的原因。

性能问题: Jakarta Validation 可以说非常轻量的,但是 Zod 并不是,如果你使用 Zod,程序会增加 100K 左右,这个对 JavaScript 应用来说还是比较客观的,建议在有性能要求的场景,不要使用 Zod。