Zod

Zod 是 TypeScript 优先的 Schema 声明和验证库。我使用的 “Schema” 一词泛指任何数据类型,从简单的字符串到复杂的嵌套对象。

Zod 的设计尽可能方便开发人员使用。我们的目标是消除重复的类型声明。有了 Zod,你只需声明一次验证器,Zod 就会自动推断出 TypeScript 的静态类型。将较简单的类型组成复杂的数据结构也很容易。

@gqloom/zod 提供了 GQLoom 与 Zod 的集成,以便将 Zod Schema 编织成 GraphQL Schema。

安装

npm
yarn
pnpm
bun
npm install @gqloom/core zod @gqloom/zod

定义简单标量

在 GQLoom 中,可以使用 zodSilk 将 Zod Schema 作为丝线使用:

import { z } from "zod" import { zodSilk } from "@gqloom/zod" const StringScalar = zodSilk(z.string()) // GraphQLString const BooleanScalar = zodSilk(z.boolean()) // GraphQLBoolean const FloatScalar = zodSilk(z.number()) // GraphQLFloat const IntScalar = zodSilk(z.number().int()) // GraphQLInt

解析器 | Resolver

为了将 Zod Schema 作为丝线使用,我们需要为其包裹 zodSilk,在开发中大量的包裹可能会显得有些繁琐,因此 @gqloom/zod 提供了重新导出的解析器和操作构造函数来简化这个过程。 从 @gqloom/zod 引入的 resolverquerymutationfield 将在内部自动包裹 zodSilk,这样在大部分情况下,我们可以直接使用 Zod Schema。

import { z } from "zod" import { resolver, query } from "@gqloom/zod" export const HelloResolver = resolver({ hello: query(z.string(), () => "Hello, World!"), })

定义对象

我们可以使用 Zod 定义对象,并将其作为丝线使用:

import { z } from "zod" import { collectNames } from "@gqloom/zod" export const Cat = z.object({ name: z.string(), age: z.number().int(), loveFish: z.boolean().nullish(), }) collectNames({ Cat })

名称和更多元数据

为对象定义名称

GQLoom 中,我们有多种方法来为对象定义名称。

使用__typename 字面量

import { z } from "zod" export const Cat = z.object({ __typename: z.literal("Cat").nullish(), name: z.string(), age: z.number().int(), loveFish: z.boolean().nullish(), })

在上面的代码中,我们使用 __typename 字面量来为对象定义名称。我们还将 __typename 字面量设置为 nullish,这意味着 __typename 字段是可选的,如果存在,则必须为 "Cat"。

import { z } from "zod" export const Cat = z.object({ __typename: z.literal("Cat"), name: z.string(), age: z.number().int(), loveFish: z.boolean().nullish(), })

在上面的代码中,我们仍旧使用 __typename 字面量来为对象定义名称,但这次我们将 __typename 字面量设置为 "Cat",这意味着 __typename 字段是必须的,且必须为 "Cat",当使用 GraphQL interfaceunion 时,必填的 __typename 将非常有用。

使用collectNames

import { z } from "zod" import { collectNames } from "@gqloom/zod" export const Cat = z.object({ name: z.string(), age: z.number().int(), loveFish: z.boolean().nullish(), }) collectNames({ Cat })

在上面的代码中,我们使用 collectNames 函数来为对象定义名称。collectNames 函数接受一个对象,该对象的键是对象的名称,值是对象本身。

import { z } from "zod" import { collectNames } from "@gqloom/zod" export const { Cat } = collectNames({ Cat: z.object({ name: z.string(), age: z.number().int(), loveFish: z.boolean().nullish(), }), })

在上面的代码中,我们使用 collectNames 函数来为对象定义名称,并将返回的对象解构为 Cat 并导出。

使用asObjectType

import { z } from "zod" import { asObjectType } from "@gqloom/zod" export const Cat = z .object({ name: z.string(), age: z.number().int(), loveFish: z.boolean().nullish(), }) .superRefine(asObjectType({ name: "Cat" }))

在上面的代码中,我们使用 asObjectType 函数创建一个元数据并将其传入 superRefine() 中来为对象定义名称。asObjectType 函数接受完整的 GraphQL 对象类型定义,并返回一个元数据。

添加更多元数据

通过 asObjectType 函数,我们可以为对象添加更多元数据,例如 descriptiondeprecationReasonextensions 等。

import { z } from "zod" import { asObjectType } from "@gqloom/zod" export const Cat = z .object({ name: z.string(), age: z.number().int(), loveFish: z.boolean().nullish(), }) .superRefine( asObjectType({ name: "Cat", description: "A cute cat", }) )

在上面的代码中,我们为 Cat 对象添加了一个 description 元数据,该元数据将在 GraphQL Schema 中呈现:

"""A cute cat""" type Cat { name: String! age: Int! loveFish: Boolean }

我们还可以使用 asField 函数为字段添加元数据,例如 description、type 等。

import { z } from "zod" import { asFieldType, asObjectType } from "@gqloom/zod" import { GraphQLInt } from "graphql" export const Cat = z .object({ name: z.string(), age: z .number() .superRefine( asFieldType({ type: GraphQLInt, description: "How old is the cat" }) ), loveFish: z.boolean().nullish(), }) .superRefine( asObjectType({ name: "Cat", description: "A cute cat", }) )

在上面的代码中,我们为 age 字段添加了 typedescription 元数据,最终得到如下 GraphQL Schema:

"""A cute cat""" type Cat { name: String! """How old is the cat""" age: Int loveFish: Boolean }

声明接口

我们还可以使用 asObjectType 函数来声明接口,例如:

import { z } from "zod" import { asObjectType } from "@gqloom/zod" const Fruit = z .object({ __typename: z.literal("Fruit").nullish(), name: z.string(), color: z.string(), prize: z.number(), }) .describe("Some fruits you might like") const Orange = z .object({ name: z.string(), color: z.string(), prize: z.number(), }) .superRefine(asObjectType({ name: "Orange", interfaces: [Fruit] }))

在上面的代码中,我们使用 asObjectType 函数创建了一个接口 Fruit,并使用 interfaces 选项将 Orange 对象声明为 Fruit 接口的实现。

省略字段

我们还可以使用 asField 函数将 type 设置为 null 来省略字段,例如:

import { z } from "zod" import { asField } from "@gqloom/zod" const Dog = z.object({ __typename: z.literal("Dog").nullish(), name: z.string().nullish(), birthday: z .date() .nullish() .superRefine(asField({ type: null })), })

将得到如下 GraphQL Schema:

type Dog { name: String }

定义联合类型

使用 z.discriminatedUnion

我们推荐使用 z.discriminatedUnion 来定义联合类型,例如:

import { z } from "zod" import { asUnionType } from "@gqloom/zod" const Cat = z.object({ __typename: z.literal("Cat"), name: z.string(), age: z.number(), loveFish: z.boolean().optional(), }) const Dog = z.object({ __typename: z.literal("Dog"), name: z.string(), age: z.number(), loveBone: z.boolean().optional(), }) const Animal = z .discriminatedUnion("__typename", [Cat, Dog]) .superRefine(asUnionType("Animal"))

在上面的代码中,我们使用 z.discriminatedUnion 函数创建了一个联合类型。对于 Animal 来说,它通过 __typename 字段来区分具体的类型。

使用 z.union

我们还可以使用 z.union 来定义联合类型:

import { z } from "zod" import { asUnionType, collectNames } from "@gqloom/zod" const Cat = z.object({ name: z.string(), age: z.number(), loveFish: z.boolean().optional(), }) const Dog = z.object({ name: z.string(), age: z.number(), loveBone: z.boolean().optional(), }) const Animal = z.union([Cat, Dog]).superRefine( asUnionType({ name: "Animal", resolveType: (it) => (it.loveFish ? "Cat" : "Dog"), }) ) collectNames({ Cat, Dog, Animal })

在上面的代码中,我们使用 z.union 函数创建了一个联合类型。对于 Animal 来说,我们通过 resolveType 函数来区分具体的类型。 在这里,如果一个动物它喜欢鱼,那么它就是一只猫,否则就是一只狗。

定义枚举类型

我们可以使用 z.enumz.nativeEnum 定义枚举类型。

使用 z.enum

通常,我们更推荐使用 z.enum 来定义枚举类型,例如:

import { z } from "zod" import { asEnumType } from "@gqloom/zod" export const Fruit = z.enum(["apple", "banana", "orange"]).superRefine( asEnumType({ name: "Fruit", valuesConfig: { apple: { description: "red" }, banana: { description: "yellow" }, orange: { description: "orange" }, }, }) ) export type IFruit = z.infer<typeof Fruit>

使用 z.nativeEnum

我们还可以使用 z.nativeEnum 来定义枚举类型,例如:

import { z } from "zod" import { asEnumType } from "@gqloom/zod" enum FruitEnum { apple, banana, orange, } export const Fruit = z.nativeEnum(FruitEnum).superRefine( asEnumType({ name: "Fruit", valuesConfig: { apple: { description: "red" }, banana: { description: "yellow" }, orange: { description: "orange" }, }, }) ) export type IFruit = z.infer<typeof Fruit>

自定义类型映射

为了适应更多的 Zod 类型,我们可以拓展 GQLoom 为其添加更多的类型映射。

首先我们使用 ZodWeaver.config 来定义类型映射的配置。这里我们导入来自 graphql-scalarsGraphQLDateTimeGraphQLJSONGraphQLJSONObject 标量,当遇到 dateanyrecord 类型时,我们将其映射到对应的 GraphQL 标量。

import { GraphQLDateTime, GraphQLJSON, GraphQLJSONObject, } from "graphql-scalars" import { z } from "zod" import { ZodWeaver } from "@gqloom/zod" export const zodWeaverConfig = ZodWeaver.config({ presetGraphQLType: (schema) => { if (schema instanceof z.ZodDate) return GraphQLDateTime if (schema instanceof z.ZodAny) return GraphQLJSON if (schema instanceof z.ZodRecord) return GraphQLJSONObject }, })

在编织 GraphQL Schema 时传入配置到 weave 函数中:

import { weave } from "@gqloom/zod" export const schema = weave(zodWeaverConfig, HelloResolver)

默认类型映射

下表列出了 GQLoom 中 Zod 类型与 GraphQL 类型之间的默认映射关系:

Zod 类型GraphQL 类型
z.array()GraphQLList
z.string()GraphQLString
z.string().cuid()GraphQLID
z.string().cuid2()GraphQLID
z.string().ulid()GraphQLID
z.string().uuid()GraphQLID
z.literal("")GraphQLString
z.literal(false)GraphQLBoolean
z.literal(0)GraphQLInt
z.number()GraphQLFloat
z.number().int()GraphQLFloat
z.boolean()GraphQLBoolean
z.object()GraphQLObjectType
z.enum()GraphQLEnumType
z.nativeEnum()GraphQLEnumType
z.union()GraphQLUnionType
z.discriminatedUnion()GraphQLUnionType