2. GraphQL类型系统与Schema设计

GraphQL 的核心竞争力并不在“查询语法”,而在于它有一套强类型、可自描述的类型系统。服务端用 Schema 定义“数据长什么样、能查什么、能改什么”,客户端据此获得自动补全、字段校验和文档。可以说,Schema 就是前后端之间的一份可执行契约。本节围绕标量类型、对象类型、枚举、接口、联合类型、输入类型、非空与列表修饰符,以及 Schema 定义语言(SDL)展开,帮助你把业务模型翻译成一份结构清晰、约束准确的 Schema。

一、标量类型:数据的最小单元

标量是 Schema 的叶子节点,代表不可再分的具体值。GraphQL 内置五个标量:Int(32 位有符号整数)、Float(双精度浮点)、String(UTF-8 字符串)、Boolean(true/false)、ID(唯一标识,序列化为字符串)。它们决定了字段的取值边界与序列化方式。

内置标量往往不够表达业务语义,例如时间、金额、URL。此时应自定义标量,并在解析器中实现序列化与校验逻辑。例如定义一个 DateTime 标量,约定使用 ISO-8601 字符串:

scalar DateTime

type Article {
  id: ID!
  title: String!
  publishedAt: DateTime
}

自定义标量的价值在于:把“格式约定”从口头文档变成类型约束,客户端能明确知道该字段返回的是可解析的时间字符串,而不是任意文本。

二、对象类型与字段:业务模型的骨架

对象类型由若干具名字段组成,每个字段都有自己的类型。字段类型可以是标量、枚举、其他对象、接口或联合类型。对象类型之间通过字段引用形成图结构,这正是 GraphQL 能按需取数的原因。

type User {
  id: ID!
  name: String!
  email: String
  articles: [Article!]!
}

type Article {
  id: ID!
  title: String!
  author: User!
  tags: [String!]
}

注意 User.articles 返回 [Article!]!:列表本身非空,且列表内元素也非空。这种“双层非空”是 GraphQL 中非常常见的表达,能有效减少客户端对 null 的防御性判断。

三、枚举、接口与联合类型

枚举(Enum)用于限定字段只能取一组预定义值,例如文章状态:

enum ArticleStatus {
  DRAFT
  PUBLISHED
  ARCHIVED
}

枚举比字符串更安全:拼写错误会在 Schema 校验阶段暴露,而不是等到运行时。

接口(Interface)定义一组字段契约,多个对象类型可以实现它。查询接口类型时,客户端可用内联片段或具名片段按具体类型取字段:

interface Node {
  id: ID!
}

type Article implements Node {
  id: ID!
  title: String!
}

type Video implements Node {
  id: ID!
  duration: Int!
}

联合类型(Union)表示“结果可能是若干类型之一”,但不要求它们共享字段。它适合搜索结果这类异构集合:

union SearchResult = Article | Video | User

type Query {
  search(keyword: String!): [SearchResult!]!
}

接口强调“共同字段”,联合强调“可能类型”,二者都依赖片段来按类型取数,区别在于接口本身可定义字段,联合不能。

四、输入类型:写操作的参数载体

查询参数可以是标量,但变更(Mutation)往往需要一组结构化参数。GraphQL 规定:输入位置只能使用输入类型(Input Object),不能直接使用对象类型。输入类型只包含标量和输入类型字段,不能引用对象类型。

input CreateArticleInput {
  title: String!
  content: String!
  tags: [String!]
  status: ArticleStatus = DRAFT
}

type Mutation {
  createArticle(input: CreateArticleInput!): Article!
}

把参数收拢为一个 input 对象,好处是字段可增可减、默认值集中、客户端调用形式稳定。示例中 status 默认 DRAFT,调用方不传即按草稿创建。

五、非空与列表修饰符

修饰符是 Schema 中极易出错的部分,需要牢记两条规则:! 表示非空,[] 表示列表,二者可组合出四种语义:

写法含义
[String]列表可为 null,元素也可为 null
[String!]列表可为 null,元素不可为 null
[String]!列表不可为 null,元素可为 null
[String!]!列表与元素都不可为 null

设计时应遵循“能非空就非空”的原则,但也要谨慎:一旦字段声明为非空,解析器返回 null 就会触发错误并向上冒泡,可能使整个响应字段变为 null。因此对确实可能缺失的数据(如未填写邮箱),应保留可空。

六、SDL:把类型写成可读文本

Schema 定义语言(SDL)是描述 Schema 的文本格式,也是本节所有示例采用的写法。它支持类型定义、字段、参数、默认值、描述文档和指令。描述文档用三引号字符串书写,会出现在 GraphQL 文档和 IDE 提示中:

"""
文章状态
"""
enum ArticleStatus {
  DRAFT
  PUBLISHED
}

type Article {
  "文章标题"
  title: String!
  status: ArticleStatus!
}

下面这张图把本节的核心类型关系组织在一起,便于对照理解:

GraphQL 类型系统核心关系 标量 Scalar Int / Float / String Boolean / ID 自定义:DateTime 枚举 Enum ArticleStatus DRAFT/PUBLISHED 对象类型 Object User / Article / Video 字段可引用其他类型 构成数据图结构 id: ID! title: String! 接口 Interface Node { id: ID! } 共享字段契约 联合 Union SearchResult = Article | Video | User 输入类型 Input CreateArticleInput 仅用于参数位置 可设默认值 修饰符 String String! [String] [String!] [String]! [String!]! 非空 + 列表组合

七、设计实践要点

第一,命名要一致。类型用大驼峰,字段用小驼峰,枚举值用全大写,这是社区通行约定。第二,输入与输出分离:对象类型用于输出,输入类型用于参数,不要试图复用。第三,非空要克制:对“业务上必然存在”的字段加 !,对“可能缺失”的字段保持可空,避免因一个 null 导致整段响应失败。第四,善用描述文档,把字段含义写进 SDL,让文档随 Schema 自动生成。第五,接口与联合按需选择:需要共享字段用接口,需要表达异构集合用联合。

设计 Schema 时可以先画数据图:把业务实体写成对象类型,把实体间引用写成字段,再为每个字段确定标量、枚举或引用类型,最后补上非空与列表修饰符。这样得到的 Schema 通常比直接写 SDL 更完整、更少遗漏。