读完这篇,你能做什么
能为十二份响应标出失败发生在哪一层,并检查权限不足或记录不存在时,系统不会靠模型编造参数继续执行。
学习理由、准备要求与知识点
为什么学它
一个结果能解析、字段类型也正确,业务上仍可能不能用。三层校验让这些问题在存储或执行工具前被发现。
先读这些内容
知识点清单
- JSON 语法校验
- Schema 结构校验
- 业务语义校验
- required 与
null - 字符串空白及长度
- 数值范围与额外字段
- default 的说明性质
- 可修复错误与权限拒绝
运行前先确认完整算法使用 Java 17 标准库;JDK 源码基线为 OpenJDK 17 GA。示例是内存中的机制模型,外部服务接入需要另外配置和验证。
01先弄懂它是什么
收到一份 JSON 后,可以按三层检查:能不能解析,字段和类型对不对,业务上能不能做。比如 count 是整数三,格式和结构都没问题,但库存只有二,仍不能购买;还要检查用户身份和资源归属。因此“JSON 合法”只说明其中一部分检查通过,不能代替完整业务判断。
字段没出现、值是 null、值是空字符串,也要分开处理。required 只要求属性存在,类型规则决定能不能是 null,长度和空白规则再决定空字符串是否合格。JSON Schema 的 default 通常只是说明默认值,不会让普通验证器自动填上;如果应用需要补默认值,应明确由哪段代码、在什么时候完成。
正式契约应决定额外字段是拒绝、忽略还是保留。严格拒绝能暴露拼写错误,也会影响前后端版本兼容;忽略便于演进,却可能掩盖调用方误以为某个字段已经生效。无论选择哪种,都需要让测试明确展示行为。
文中这几个词,先认一认
- 属性存在性
- 对象里是否真的有这个键。例如没有 name 字段,与有 name 但值为
null,是两种情况。 - Schema
- 规定数据长什么样、需要满足什么限制的规范。本篇只演示其中一个明确的小范围。
- 业务不变量
- 合法操作前后都必须满足的业务条件,例如用户不能跨权限读取他人的资料。
02看看它怎么工作
本题约定输入Map只允许title和count,二者必填。title是非空白且不超过八十Unicode码点的String;count必须是Java Integer且在一到五之间。这个内部DTO规则故意明确,不冒充完整JSON Schema:JSON数值一与一点零的Schema整数语义要由正式解析和验证库处理。
先检查未知键与必填,再检查存在字段的值。缺title只报告required,不再同时报告type,避免同一根因产生无意义重复;存在title但值为null则报告type。错误按固定字段顺序生成,未知键先排序,结果不依赖Map迭代顺序。
结构通过后再进入业务Service,使用可信身份查资源并检查动作条件。模型可以修复拼错字段,但不能替用户获得权限;默认值也不能覆盖用户明确提交的禁止值。把验证结果作为控制流输入,失败时应阻止后续副作用,而不是只写一条日志继续执行。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart LR
J["原始 JSON"] --> P["语法解析"]
P --> S["结构与存在性"]
S --> V["业务字段规则"]
V --> A["可信身份与当前状态"]
A --> E["允许的副作用"]按顺序看,每一步做了什么
- 列出允许字段、必填、
null、空白与范围规则。 - 通过成熟库完成JSON语法解析和正式Schema验证。
- 在内部DTO边界区分缺失与存在但无效。
- 返回稳定错误路径,决定拒绝或受限修复。
- 结构通过后继续做可信身份与业务状态验证。
03对照实现代码
OpenJDK 17 GA,jdk-17+35 · Map.getOrDefault / containsKey
下方为原创 Java 教学片段,调用已核验的 JDK API 说明局部机制;不是该类原样源码,也不实现神经网络。完整程序见算法模块。
boolean present = input.containsKey("title"); // 键是否存在,与 get 的结果分开判断。
Object value = input.get("title");
if (!present) errors.add("title.required");
else if (!(value instanceof String)) errors.add("title.type"); // 显式 null 也走类型错误。
// 默认值是否写入对象,是业务转换步骤,不是 containsKey 自动完成的功能。Map.getOrDefault的默认逻辑同时考虑get结果和containsKey,正是因为允许null的Map无法仅靠get返回null区分缺失与显式null。
不同Map实现对null有不同契约,例如某些并发映射不允许null。输入反序列化后使用哪种容器需要明确,本题用允许null的HashMap构造相关边界。
Map接口负责键值访问,不执行JSON Schema。示例借助真实API说明存在性判断,再由应用写出有限字段规则,避免把一个小验证器称为完整规范实现。
类型检查、错误路径和默认值政策共同组成应用契约。若依赖自动类型转换,应另外测试字符串数字、浮点值和超大整数,否则宽松转换可能掩盖错误输入。
代码先调用谁,再调用谁
- 解析后的对象进入
Map边界 containsKey检查存在性- get读取实际值
- 有限字段规则聚合错误
- 通过后交给业务授权层
04动手写一个例子
有限对象契约验证
输入Map,只允许title和count;title为一至八十码点非空白字符串,count为一至五的Java Integer。输出稳定错误列表;不自动填default。Java17 Main.java运行固定自测。
- 输入 1
title=Java,count=3- 输出 1
无错误
- 输入 2
title=null,count=9,extra=true- 输出 2
extra.additional、title.type、count.range
允许集合用于识别未知字段,未知键排序使错误报告稳定。必填检查使用containsKey,避免把显式null混成字段缺失。
对已存在字段先检查类型,再检查类型相关约束。只有String才计算码点长度,只有Integer才比较范围,避免强制类型转换异常掩盖预期错误报告。
Unicode码点比UTF-16代码单元更接近JSON Schema字符串长度语义,但仍不是用户感知的字形簇。产品若按可见字符限制,需要额外分词规则,不能直接宣称码点等于视觉字符。
为什么这样写是对的每条错误只对应一个已定义的契约违规;类型不合格时不执行依赖该类型的范围检查,验证过程不向输入填字段或修改值。
k为键数,未知键排序贡献O(k log k),C为title字符长度;固定业务字段检查为常数。输出与未知键列表占O(k),不包含原始JSON解析成本。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
import java.util.*;
public class Main {
static List<String> validate(Map<String,Object> input){
List<String> e=new ArrayList<>();Set<String> allowed=Set.of("title","count");
for(String key:new TreeSet<>(input.keySet()))if(!allowed.contains(key))e.add(key+".additional"); // 稳定未知字段次序。
if(!input.containsKey("title"))e.add("title.required");
else if(!(input.get("title") instanceof String s))e.add("title.type");
else if(s.isBlank()||s.codePointCount(0,s.length())>80)e.add("title.length"); // 按码点,不按 UTF-16 单元。
if(!input.containsKey("count"))e.add("count.required");
else if(!(input.get("count") instanceof Integer n))e.add("count.type");
else if(n<1||n>5)e.add("count.range"); // 类型合法后才比较业务范围。
return e; // 不把默认值写回输入,保持验证无副作用。
}
static void check(List<String> expected,Map<String,Object> input){List<String> got=validate(input);if(!expected.equals(got))throw new AssertionError(got);}
public static void main(String[] args){
check(List.of(),Map.of("title","Java","count",3));
check(List.of("title.required","count.required"),Map.of());
Map<String,Object> bad=new HashMap<>();bad.put("title",null);bad.put("count",9);bad.put("extra",true);
check(List.of("extra.additional","title.type","count.range"),bad);
check(List.of("count.type"),Map.of("title","Java","count",1.0)); // 内部 DTO 规则与完整 JSON Schema 分开。
check(List.of("title.length"),Map.of("title"," ","count",1));
check(List.of(),Map.of("title",new String(Character.toChars(0x1F600)).repeat(80),"count",5));
System.out.println("PASS 6 tests");
}
}这些特殊情况也要试一试
- 输入来自JSON对象,键约定为非
null字符串;其他Map调用方需先满足此边界。 - 显式
null与缺失返回不同错误。 - Java
Integer限制是内部表示规则,不代表JSON Schema拒绝所有1.0形式。
进阶选读 · 后续再读:契约演进与兼容性检查
场景新版本增加字段或收紧范围,旧客户端与已存历史对象仍存在。
做法为请求、响应和持久数据分别定义版本迁移;比较旧合法集合是否仍被新规则接受,并用历史样本回放验证。
复杂度有限枚举域可穷举,复杂Schema包含组合结构,兼容性检查成本依规则语言而变;样本通过不是全部输入的形式证明。
好处与代价严格边界便于发现错误,兼容演进要求显式迁移;无限宽松会让配置拼写错误长期隐藏。
05项目里怎么用
把验证器放在副作用之前,包括工具调用、数据库写入和对外请求。失败返回稳定代码及安全字段路径,日志保留请求关联信息。若只在展示层提醒“字段有误”,后台已经执行动作,验证就没有承担真正控制边界。
默认值应由明确转换函数处理,并区分未提供与用户明确提供null或零。为缺count填三可以是产品规则,但如果用户提交零却被覆盖成三,就掩盖了错误。转换前后记录结构版本,而不是依赖某个库恰好采用的宽松行为。
权限和资源状态随时间变化,结构验证后到执行前仍需核对可信身份及当前版本。模型提供的userId不能直接作为授权依据。即使输出格式完全合规,也不能跳过归属、额度和幂等检查;结构合法只是进入业务处理的必要条件之一。
容易踩的坑
- required被误解成字段必须非
null。 - 认为default会由所有Schema验证器自动填值。
- 类型转换宽松却没有测试字符串数字和超大整数。
- 验证失败仅记日志,后续写操作仍继续。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 严格拒绝未知字段 | 及时发现拼写与配置错误 | 演进时需协调版本 | 内部工具与高约束写接口 |
| 忽略未知字段 | 便于前后端渐进升级 | 调用方可能误以为字段生效 | 有明确兼容策略的读接口 |
| 显式迁移转换 | 默认和版本变化可审计 | 需要维护迁移与回放样本 | 长期存储的任务状态 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01required与非null相同吗?
展开参考答案与推导
不同。required要求键存在,允许哪些值由type等规则决定;显式null可能满足存在性却违反类型。
02default的作用是什么?
展开参考答案与推导
在JSON Schema中通常提供注解信息,不要求验证器修改实例;自动填充是额外实现或应用政策。
03结构校验为何不能代替授权?
展开参考答案与推导
结构只说明字段形态,不能证明调用者有权访问该资源;授权必须基于可信身份和实际归属。
换个情况,还会处理吗
01请求多写了一个拼错字段却成功,怎样判断问题?
展开参考答案与推导
先看契约是否允许额外字段与解析器是否忽略未知属性;若要求严格,应增加拒绝配置和对应回归样本。
02count缺失自动变零,导致错误归因怎么办?
展开参考答案与推导
在原始映射或可空DTO边界保留缺失状态,先报required,再执行显式默认转换,避免原始状态被反序列化默认值抹掉。
03模型将userId改成管理员格式,校验通过能执行吗?
展开参考答案与推导
不能。身份来自会话或可信上下文,模型参数只是输入数据;执行前用真实身份检查资源权限。
写代码试一试
01null标题应返回什么?
- 输入 / 条件
title=null,count=1- 输出 / 预期
title.type
提示 先检查存在性。
展开参考答案与推导
键存在所以不是required错误;值不是String,报告type且不继续调用字符串长度方法。
02如何验证没有默认填充?
- 输入 / 条件
输入空Map- 输出 / 预期
两个required错误,Map仍为空
提示 比较调用前后。
展开参考答案与推导
验证器只返回错误;若要填默认值,另写转换函数并对其作用范围与版本单独测试。
03表情长度为什么不用length?
- 输入 / 条件
80个补充平面表情- 输出 / 预期
码点数80,UTF-16长度160
提示 使用codePointCount。
展开参考答案与推导
Java String.length统计代码单元,本题按码点限制;组合表情仍可能含多个码点,不能声称等于视觉字数。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能为十二份响应标出失败发生在哪一层,并检查权限不足或记录不存在时,系统不会靠模型编造参数继续执行。
本篇重点加练
业务语义和权限拒绝的边界决定修复机制是否合适,需要用状态相关规则检验。
- 业务语义校验
- 可修复错误与权限拒绝
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
给任务JSON设计语法层与Schema层检查,举出能通过其中一层却不能通过另一层的输入,并说明你自己的错误分类。
- 提交内容
- 一份独立书面回答或标注图,包含自造例子、概念边界及判断依据。
- 怎样算完成
- 逐项回应题目要求,区分事实、假设与个人判断。
- 能用自己的例子解释适用条件,记录尚未弄懂的点。
练到的知识点JSON 语法校验 · Schema 结构校验
- 作业 02代码推演
对缺失字段、
null、空字符串和已填值四种任务记录,推演业务校验过程;明确required的约束对象和状态条件。- 提交内容
- 手工推演记录,写明输入约定、中间状态、最终判断;可运行时附程序对照记录。
- 怎样算完成
- 推演包含中间过程,数据形状、单位、顺序或状态前后一致。
- 主动检查至少一个边界或异常条件,能解释手算与运行差异。
练到的知识点业务语义校验 · required 与 null
- 作业 03原理问答
为学习计划操作定义状态相关规则:相同字段值在草稿、已确认、已取消状态下是否允许相同操作?列出判定所需事实与不可自动修复的情形。
- 提交内容
- 概念或合同说明、关系图与自造案例,明确事实、假设和适用条件。
- 怎样算完成
- 关键概念有明确边界,能用案例检验声明。
- 未获证据支持的假设单独标注。
练到的知识点业务语义校验 · 可修复错误与权限拒绝
综合应用
- 作业 04面试场景
计划标题全为空白、时长为-5、附带未知字段。设计各项处理规则,说明拒绝、归一化与保留原输入的依据。
- 提交内容
- 场景分析与决策记录,包含信息缺口、约束、备选方案和验证计划。
- 怎样算完成
- 方案回应给定约束,列出至少一种替代方案及代价。
- 每项关键结论说明所需证据,不能核实的部分明确标注。
练到的知识点字符串空白及长度 · 数值范围与额外字段
- 作业 05编程实践
实现校验流水线,Schema含default说明且输入缺字段;另外模拟无权限写操作,测试修复入口与权限判断不会被混成同一条路径。
- 提交内容
- 可复现的最小程序或实验材料、自写测试、环境说明与运行记录;题目允许纸面实验时提交完整案例表。
- 怎样算完成
- 提交物按说明可以重现;测试覆盖正常、边界与失败场景。
- 运行证据与断言能核验声明的行为,注明模拟与真实执行范围。
练到的知识点default 的说明性质 · 可修复错误与权限拒绝
- 作业 06排错实验
建立一份边界输入表,覆盖极长字符串、数值边界、空集合和交叉字段冲突,逐项记录由哪一层发现问题。
- 提交内容
- 最小故障输入、复现步骤、观测记录、定位依据及回归用例。
- 怎样算完成
- 其他人可按记录复现故障,并区分现象与已验证原因。
- 定位过程有观测依据,回归用例可以再次暴露原问题。
练到的知识点Schema 结构校验 · 业务语义校验 · 字符串空白及长度
- 作业 07编程实践
实现本地业务校验器,输入都通过JSON语法和Schema检查,但操作状态与用户权限各不相同;检查错误分类与修复入口是否符合合同。
- 提交内容
- 可复现的小程序或题目允许的离线实验、自写测试、输入和运行记录。
- 怎样算完成
- 输入、环境和运行过程可重现,测试覆盖声明的重点条件。
- 验证目标由合同确定,模拟结果不冒充真实服务证据。
练到的知识点业务语义校验 · 可修复错误与权限拒绝
- 作业 08排错实验
在权限检查后改变目标记录的状态,再提交原先“可修复”的结果;记录语义验证使用的版本及检查后变更带来的未决问题。
- 提交内容
- 最小故障序列、观测记录、原因核对与可再次触发原问题的测试。
- 怎样算完成
- 故障可以稳定重现,现象与已验证原因分开。
- 观测记录足以判断声明的合同是否被违反。
练到的知识点业务语义校验 · 可修复错误与权限拒绝
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
为移动端旧客户端设计契约升级策略,区分新增必填、范围收紧与额外字段政策变化,并制定历史数据重放实验。
- 提交内容
- 设计评审文档或对照实验报告,包含前置条件、方案取舍、验收证据与尚未验证的范围。
- 怎样算完成
- 比较使用一致条件与事先声明的标准,记录失败与未知结果。
- 说明方案的适用前提、未覆盖风险及后续验证方法。
练到的知识点JSON 语法校验 · required 与 null · default 的说明性质
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 反例构造
为“修复后的数据通过全部字段检查,就可以重试原操作”构造一个状态或权限改变的反例,给出判定需要的额外证据。
- 提交内容
- 具体反例、原主张的适用前提、复现或逻辑证明及结论边界。
- 怎样算完成
- 反例满足原主张所声称的前提,确实针对其结论。
- 说明一个反例能推翻什么、不能进一步证明什么。
练到的知识点业务语义校验 · 可修复错误与权限拒绝
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
JSON Schema:对象约束 (新标签页)↗
重点阅读 properties、required、
additionalProperties与缺字段示例,建立最小输入或输出契约。 - 实践参考文档英文
JSON Schema:数值约束 (新标签页)↗
查看 integer、number、minimum 和 maximum 的行为,用边界值验证数量、范围等参数。
- 深入参考文档英文
JSON Schema:字符串约束 (新标签页)↗
阅读
minLength、maxLength、pattern 和 format 的说明,区分语法形状与真实业务合法性。 - 深入参考文档英文
Spring AI:Output Converters (新标签页)↗
阅读转换器工作方式及尽力转换的限制,把结构校验与反序列化分开验证。
- 实践实践指南中文
FastAPI 接口入门:把 Python 数据接成可调用的服务 (新标签页)↗
先读创建数据模型和声明请求参数。小练习:做一个只返回学习记录的本地接口,发送正常数据、缺少必填字段和错误类型三种请求,记录响应差别;暂不接真实模型。 文档公开,示例标有 Python 3.10+。中文页包含人类引导的 AI 翻译,疑义可对照英文。
- 深入参考文档英文
OpenJDK 17 Map 源码 (新标签页)↗
按指定版本定位
Map.getOrDefault/containsKey;对照源码分析理解实际调用边界。