时间、金额、ID、枚举、Null、分页与错误码:跨语言数据怎样保持语义
字段类型都叫 string 或 number,为什么跨语言后数值和状态会悄悄改变? 这类事故通常不是序列化库突然失效,而是双方只共享了字段形状,没有共享字段身份、业务含义、状态前提和演进规则。线格式类型只是容器,业务契约还必须定义单位、精度、范围、时区、presence、排序和未知值策略。金额不能依赖二进制浮点,时间不能丢失 instant 与 zone 的区别。
契约不是一份文档,而是跨时间的承诺
金额在边界上使用 decimal string 或带 currency 的最小单位整数;时间区分 Instant、OffsetDateTime、LocalDate 与业务时区;ID 当不透明字符串;枚举保留 UNKNOWN;Null、缺失和显式清空使用可判定 presence;游标绑定排序与快照。
一次契约调用怎样从制品走到业务状态
每个公共字段记录 wire type、业务类型、单位、范围、示例、缺失语义、未知值、排序与脱敏策略。错误 code 保持机器身份,message 可本地化;分页响应给出 nextCursor 和 snapshot 语义而非虚假总页数承诺。
0.1+0.2 的浮点误差会进入签名和对账;把 long ID 交给 JavaScript number 会丢精度;本地时间没有 offset 会在夏令时歧义;新增枚举值使旧客户端穷举失败;offset 分页在并发写入时重复或遗漏。
最隐蔽的破坏往往结构完全合法:金额单位由元变成分、时间由 UTC instant 变成本地时间、枚举新增值触发旧客户端 default 分支、列表排序从稳定变成无序、错误码沿用却改变可重试性。这些变化无法只靠 JSON 语法或字段 diff 发现,必须把业务语义写成示例、属性测试与消费者断言。
契约评审应从调用者动作反推。收到成功后是否会重试、缓存或继续扣款;收到特定错误后是否可修改参数、等待、刷新认证或人工介入;缺失字段与 null 是否相同;未知枚举是忽略、保留还是拒绝;分页游标是否绑定权限和排序。只有动作可判定,契约才稳定。
身份比名字更重要
兼容性不是单向的布尔值
兼容矩阵的行是活跃消费者及版本,列是候选契约和生产者行为。矩阵单元格保存结构验证、生成编译、样本回放、错误分支与性能限制结果。只测试最新 SDK 会让旧移动端、离线任务、合作方和延迟消息成为盲区。
结构验证必须建立引用闭包
错误契约必须告诉调用方下一步
可运行模型一:让破坏性变更显形
javac --release 17 -Xlint:all -Werror MoneyPrecisionDemo.java
java MoneyPrecisionDemo预期输出:
decimal=0.3 double=0.30000000000000004 equal=false minorUnits=30 exact=true第一个模型把兼容判断压缩为稳定数量关系:新增 required、字段号复用、阻塞消费者、浮点误差或状态机非法转换。它不替代真实解析器,却能让评审者看到“哪些输入导致哪条门禁失败”,并能稳定用于回归。
修改模型输入做反向实验:把 optional 变 required、从 reserved 集合取字段号、新增旧消费者不能识别的事件、让金额经过 double 或移除问题类型。若模型仍报告 publishable,说明门禁只检查了表面结构。
可运行模型二:验证运行时仍然收敛
javac --release 17 -Xlint:all -Werror NullEnumTimeDemo.java
java NullEnumTimeDemo预期输出:
samples=6 valid=3 invalidNull=1 unknownEnum=1 invalidOffset=1 explicitPresence=true第二个模型覆盖引用缺失、deadline 传播、事件重放、presence、消费者契约或回滚制品。结构兼容只是第一道门,运行时还必须在超时、重复、乱序、缺省值和版本并存时保持可判定结果。
验收不应只断言进程退出码。需要核对输出中的失败数、阻塞消费者、最终版本、剩余预算、错误泄漏和 rollbackArtifact。任何非零破坏都应关联 owner、例外期限与迁移计划,不能以“先上线观察”替代证据。
Deadline、幂等和重试也是契约
同步调用的 deadline 必须从入口携带绝对截止时间或剩余预算,下游每跳先扣除自身安全余量再决定是否发起调用。若每层重新设置固定 timeout,三跳链路会把五百毫秒用户预算扩成数秒后台工作。客户端取消后,服务端和下游应尽快停止无用计算。
契约应声明最大请求/响应、流式背压、分页大小和批次上限。生成 SDK 若默认无限读取响应或自动重试非幂等调用,同样属于契约缺陷。性能限制必须进入规范扩展、客户端策略或配套机器可读策略,不能只藏在运维文档。
时间、金额、ID、枚举与 Null 要逐个定义
金额需要 amount 与 currency;amount 使用十进制定点或最小单位整数,并固定舍入模式。ID 是不透明标识,不参与算术,不因当前数据库是 long 就暴露为 JSON number。时间明确是 instant、带 offset 的时间、日历日期还是特定时区中的本地时间;只有日期就不要伪造午夜 UTC。
缺失、null、默认值和显式清空是四种可能语义。PATCH 尤其需要 presence:缺失表示不修改,null 可能表示清空,具体值表示设置。若语言生成器把缺失和零值合并,契约要使用 wrapper、oneof、optional 或补丁操作结构恢复可判定性。
枚举新增必须假设旧消费者会收到未知值。客户端模型保留 UNKNOWN/UNRECOGNIZED 和原始值,不应在未知值上崩溃或默认映射为某个真实业务状态。服务端在收缩枚举前先观察未知值流量,确认所有生产者完成迁移。
事件契约还要承担历史
同步 API 可以通过部署窗口逐步迁移,事件则可能在日志、重试队列、对象存储和审计归档中保留多年。消费者升级后仍要能重放旧事件;新事件投入生产后,滞后消费者仍可能是旧代码。因此事件兼容矩阵必须覆盖历史语料和最大保留期。
发布门禁要横跨生产者和消费者
消费者驱动契约不能让消费者锁死服务端实现。消费者声明自己真正依赖的请求、响应、错误和语义,生产者验证这些承诺;公共规范仍是总体边界。测试数据要覆盖 unknown field、缺失、null、最大值、未知枚举、乱序、超时和权限,而不是只保存一条成功 JSON。
安全边界必须写进 Schema 和生成器
个人数据字段标注分类、用途、日志策略和删除传播。SDK 的 toString、调试拦截器与自动日志默认脱敏;事件契约记录数据保留与擦除关联键。契约制品公开范围也要分级,内部管理接口不应因为生成文档而自动暴露。
观测要能回答哪个版本在破坏谁
发布看板至少显示活跃消费者版本分布、最老受支持版本、候选变更破坏数、SDK 生成/编译结果、provider/consumer 通过率、unknown enum、解析失败和弃用接口流量。没有消费者版本可见性,就无法安全删除旧字段或旧路径。
回滚必须能复现旧制品与旧语义
版本号是沟通工具,不是兼容机制。路径版本、媒体类型版本、RPC 新方法或事件新类型都只建立新身份;迁移、双写、流量观测、消费者支持与终止条件仍需单独设计。能兼容扩展时避免无意义大版本,确实改变语义时也不要用小版本掩盖。
架构决策必须留下可复审证据
记录契约 owner、消费者清单、权威源文件、artifact identity、支持的规范/dialect、字段身份、错误类型、幂等与 deadline、数据单位和 presence、兼容类型、发布门禁、弃用窗口、历史重放和回滚制品。每个“兼容”结论链接到具体 diff 与消费者测试。
线格式类型只是容器,业务契约还必须定义单位、精度、范围、时区、presence、排序和未知值策略。金额不能依赖二进制浮点,时间不能丢失 instant 与 zone 的区别。 这一主题最终要守住:值跨语言序列化、反序列化和重放后仍保持同一单位、精度、时间点与缺失语义。若只能证明最新生产者与最新客户端的 happy path,就还没有建立可演进契约。
契约源、解析 bundle、digest、生成器版本与测试报告可追溯。明确区分传输、结构、业务、运行和演进五层语义。所有机器身份删除后保留并禁止复用,名称不承担隐式判断。
兼容矩阵覆盖所有活跃消费者、历史样本与双向部署顺序。引用闭包、dialect、循环、外部资源和解析预算经过门禁。错误类型、状态、可重试性和 detail 脱敏具有稳定规则。
deadline、取消、幂等键、分页游标和容量上限进入契约。时间、金额、ID、枚举、Null 与未知值有跨语言语义。SDK 实际生成并编译,Provider 与 Consumer 测试均执行。
expand/migrate/contract、弃用终止条件和回滚制品经过演练。两个 Java 17 模型以严格编译运行,输出与正文逐项一致。
以下一手资料用于核对协议与规范语义,实际采用版本仍需匹配解析器、生成器和语言运行时能力:
RFC 3339: Date and Time on the Internet。RFC 9457: Problem Details for HTTP APIs。JSON Schema Validation 版本标识 2020-12
