4. 数据模型与序列化:Schema定义、数据类型约束、嵌套对象与数组、枚举与组合

数据模型,说白了就是接口的「骨架」。你想想看,如果连数据长什么样都说不清楚,那前端后端怎么协作?我见过太多团队因为数据格式没对齐,联调时吵得不可开交。今天我们就来聊聊怎么把这个骨架搭结实。

4.1 Schema定义:给数据立规矩

Schema就是一份契约。它告诉调用方:你传什么、我返什么、每个字段是什么类型、哪些必填、哪些可选。没有Schema,接口文档就是一张废纸。

我个人习惯用JSON Schema来定义RESTful接口的数据模型。它跨语言、可校验、可生成文档,一套定义到处用。举个例子:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "用户注册请求",
  "type": "object",
  "required": ["username", "email", "password"],
  "properties": {
    "username": {
      "type": "string",
      "minLength": 3,
      "maxLength": 20,
      "pattern": "^[a-zA-Z0-9_]+$"
    },
    "email": {
      "type": "string",
      "format": "email"
    },
    "password": {
      "type": "string",
      "minLength": 8,
      "maxLength": 128
    },
    "age": {
      "type": "integer",
      "minimum": 0,
      "maximum": 150
    }
  }
}

你看,每个字段的约束都写得清清楚楚。username只能包含字母数字和下划线,长度3到20位。email必须符合邮箱格式。这些约束在服务端校验一遍,客户端校验一遍,双重保险。

我的经验:Schema定义越早越好。我曾在项目中期才补Schema,结果发现三个字段命名不统一、两个字段类型对不上。改起来牵一发动全身,那叫一个痛苦。

4.2 数据类型约束:别让脏数据进门

数据类型约束是接口安全的第一道防线。你想想,如果年龄字段传了个字符串"abc",系统不崩才怪。我见过最离谱的一次,有人把手机号传成了数组,结果下游系统直接OOM。

常见的约束类型包括:

约束类型 说明 示例
类型约束 指定字段的数据类型 string, integer, number, boolean
长度约束 限制字符串或数组的长度 minLength, maxLength, minItems, maxItems
范围约束 限制数值的大小范围 minimum, maximum, exclusiveMinimum
格式约束 校验特定格式 email, uri, date-time, ipv4
正则约束 用正则表达式校验 pattern: "^[0-9]{11}$"

为什么要这么严格?因为接口一旦暴露出去,你根本不知道调用方会传什么鬼东西。我曾经遇到一个案例,客户端把用户ID传成了SQL注入语句,幸好服务端做了类型校验,直接返回400。要是没这层防护,后果不堪设想。

避坑指南:不要只依赖客户端校验。客户端校验是给正常人用的,服务端校验才是给所有人用的。我曾经见过一个团队只在前端做校验,结果被爬虫直接调接口,脏数据全进来了。

4.3 嵌套对象与数组:处理复杂结构

现实世界的数据很少是扁平的。一个订单包含多个商品,一个用户有多条地址,这些都是嵌套结构。怎么在Schema里表达?

嵌套对象用properties里再套properties。数组用items指定元素类型。看这个例子:

{
  "type": "object",
  "required": ["orderId", "items"],
  "properties": {
    "orderId": {
      "type": "string",
      "pattern": "^ORD\\d{10}$"
    },
    "items": {
      "type": "array",
      "minItems": 1,
      "maxItems": 100,
      "items": {
        "type": "object",
        "required": ["productId", "quantity", "price"],
        "properties": {
          "productId": { "type": "string" },
          "quantity": { 
            "type": "integer",
            "minimum": 1,
            "maximum": 999
          },
          "price": {
            "type": "number",
            "minimum": 0.01
          }
        }
      }
    },
    "shippingAddress": {
      "type": "object",
      "properties": {
        "province": { "type": "string" },
        "city": { "type": "string" },
        "detail": { "type": "string" }
      }
    }
  }
}

这里有几个要点:

  • 数组要限制长度。我见过有人一次传10万条数据,直接把服务端内存打爆。
  • 嵌套层级不要太深。超过3层,可读性就差了。我建议最多嵌套4层。
  • 数组元素要定义清楚。别用items: {}这种空对象,那等于没约束。

为什么会这样?因为嵌套结构一旦复杂,校验成本就上去了。你想想,一个订单有100个商品,每个商品又要校验productId、quantity、price,那就是300次校验。如果嵌套再深一层,校验次数指数级增长。

4.4 枚举与组合:限定可选范围

有些字段的值是固定的。比如订单状态:待支付、已支付、已发货、已完成、已取消。这种场景用枚举最合适。

{
  "orderStatus": {
    "type": "string",
    "enum": ["PENDING", "PAID", "SHIPPED", "COMPLETED", "CANCELLED"]
  },
  "paymentMethod": {
    "type": "string",
    "enum": ["ALIPAY", "WECHAT", "CREDIT_CARD", "BANK_TRANSFER"]
  }
}

枚举的好处是:

  • 调用方不会传错值。传了"已付款"而不是"PAID",直接报错。
  • 文档自动生成。枚举值就是接口文档的一部分。
  • 服务端不用做if-else判断。校验器帮你挡掉了非法值。

组合呢?就是oneOf、anyOf、allOf这些。它们用来表达「或」的关系。比如一个支付请求,可能是支付宝支付,也可能是微信支付,两者结构不同:

{
  "oneOf": [
    {
      "type": "object",
      "required": ["alipayAccount", "alipaySign"],
      "properties": {
        "alipayAccount": { "type": "string" },
        "alipaySign": { "type": "string" }
      }
    },
    {
      "type": "object",
      "required": ["wechatOpenId", "wechatAppId"],
      "properties": {
        "wechatOpenId": { "type": "string" },
        "wechatAppId": { "type": "string" }
      }
    }
  ]
}

这里用oneOf表示:请求必须符合其中一个结构,不能同时符合两个,也不能一个都不符合。我遇到过有人用anyOf,结果客户端传了支付宝和微信两套参数,服务端不知道该用哪个,直接报500。

关键区别:
  • oneOf:必须且只能匹配一个
  • anyOf:至少匹配一个(可以多个)
  • allOf:必须匹配所有
选哪个要看业务语义。别乱用,否则校验结果会出乎意料。

4.5 知识结构总览

下面这张图把数据模型的核心要素串起来了。你可以看到,从Schema定义出发,到数据类型约束、嵌套结构、枚举组合,每一步都在加固接口的可靠性。

数据模型与序列化知识结构 Schema 定义 数据类型约束 嵌套对象与数组 枚举与组合 序列化与反序列化 核心要点: • Schema 定义是数据模型的起点,所有约束从这里展开 • 数据类型约束、嵌套结构、枚举组合是三大支柱 • 序列化是数据模型落地的最后一步,确保传输安全

4.6 序列化:从模型到字节流

数据模型定义好了,怎么在网络里传输?这就涉及到序列化。JSON是目前最主流的序列化格式,但也不是唯一选择。

序列化格式 优点 缺点 适用场景
JSON 可读性好、跨语言、生态丰富 体积大、无类型 RESTful API、Web应用
Protocol Buffers 体积小、速度快、强类型 可读性差、需要定义.proto文件 微服务间通信、高性能场景
MessagePack 比JSON小、兼容JSON 生态不如JSON 移动端、IoT设备
Avro 支持Schema演化、压缩率高 学习成本高 大数据场景、Kafka消息

我个人偏好JSON+JSON Schema的组合。原因很简单:可读性好,调试方便。你想想,出问题的时候,抓个包就能看懂数据长什么样。要是用Protobuf,还得先反序列化,多一道工序。

但Protobuf在性能上确实有优势。我做过压测,同样的数据,Protobuf序列化后的体积只有JSON的1/3,序列化速度快了5倍。所以,内部服务间通信我建议用Protobuf,对外接口用JSON。

我的建议:不管用哪种序列化格式,Schema定义一定要有。没有Schema的序列化,就像没有图纸的施工,早晚要出事。我曾经接手过一个项目,所有接口都用JSON,但没有Schema定义。结果一个字段从string改成了number,前端没同步更新,线上出了P0故障。

4.7 实战:一个完整的Schema示例

最后,我们来看一个完整的用户管理接口Schema。它综合了前面讲的所有知识点:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "用户管理接口",
  "type": "object",
  "required": ["action", "user"],
  "properties": {
    "action": {
      "type": "string",
      "enum": ["CREATE", "UPDATE", "DELETE", "QUERY"]
    },
    "user": {
      "type": "object",
      "required": ["username", "email"],
      "properties": {
        "username": {
          "type": "string",
          "minLength": 3,
          "maxLength": 20,
          "pattern": "^[a-zA-Z0-9_]+$"
        },
        "email": {
          "type": "string",
          "format": "email"
        },
        "roles": {
          "type": "array",
          "minItems": 1,
          "maxItems": 5,
          "items": {
            "type": "string",
            "enum": ["ADMIN", "EDITOR", "VIEWER", "GUEST"]
          }
        },
        "profile": {
          "type": "object",
          "properties": {
            "nickname": { "type": "string", "maxLength": 50 },
            "avatar": { "type": "string", "format": "uri" },
            "birthday": { "type": "string", "format": "date" }
          }
        }
      }
    },
    "metadata": {
      "type": "object",
      "properties": {
        "source": {
          "type": "string",
          "enum": ["WEB", "APP", "API"]
        },
        "timestamp": {
          "type": "string",
          "format": "date-time"
        }
      }
    }
  }
}

这个Schema定义了:

  • 操作类型(枚举)
  • 用户信息(嵌套对象)
  • 角色列表(数组+枚举)
  • 个人资料(嵌套对象)
  • 元数据(嵌套对象+枚举+日期格式)

你看,所有知识点都用上了。这就是一个生产级别的Schema定义。嗯,照着这个写,基本不会出大问题。