qinghuan
← 规定输出格式并检查 / 全部笔记

单元 11 / 规定输出格式并检查

P0 · 必会主线L1 · 基础

让结构化结果通过三层校验

数据能解析成 Java 对象后,继续检查必填、空值、范围和资源权限,确认业务上能否执行。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能为十二份响应标出失败发生在哪一层,并检查权限不足或记录不存在时,系统不会靠模型编造参数继续执行。

学习理由、准备要求与知识点

为什么学它

一个结果能解析、字段类型也正确,业务上仍可能不能用。三层校验让这些问题在存储或执行工具前被发现。

先读这些内容

知识点清单

  • 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["允许的副作用"]

按顺序看,每一步做了什么

  1. 列出允许字段、必填、null、空白与范围规则。
  2. 通过成熟库完成JSON语法解析和正式Schema验证。
  3. 在内部DTO边界区分缺失与存在但无效。
  4. 返回稳定错误路径,决定拒绝或受限修复。
  5. 结构通过后继续做可信身份与业务状态验证。

03对照实现代码

OpenJDK 17 Map 源码 (新标签页)↗

OpenJDK 17 GA,jdk-17+35 · Map.getOrDefault / containsKey

下方为原创 Java 教学片段,调用已核验的 JDK API 说明局部机制;不是该类原样源码,也不实现神经网络。完整程序见算法模块。

核心机制代码 · java
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说明存在性判断,再由应用写出有限字段规则,避免把一个小验证器称为完整规范实现。

类型检查、错误路径和默认值政策共同组成应用契约。若依赖自动类型转换,应另外测试字符串数字、浮点值和超大整数,否则宽松转换可能掩盖错误输入。

代码先调用谁,再调用谁

  1. 解析后的对象进入Map边界
  2. containsKey检查存在性
  3. get读取实际值
  4. 有限字段规则聚合错误
  5. 通过后交给业务授权层

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字符串长度语义,但仍不是用户感知的字形簇。产品若按可见字符限制,需要额外分词规则,不能直接宣称码点等于视觉字符。

为什么这样写是对的每条错误只对应一个已定义的契约违规;类型不合格时不执行依赖该类型的范围检查,验证过程不向输入填字段或修改值。

时间 O(k log k+C)额外空间 O(k)

k为键数,未知键排序贡献O(k log k),C为title字符长度;固定业务字段检查为常数。输出与未知键列表占O(k),不包含原始JSON解析成本。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

Main.java · 完整可运行程序 · Java
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 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能为十二份响应标出失败发生在哪一层,并检查权限不足或记录不存在时,系统不会靠模型编造参数继续执行。

本篇重点加练

业务语义和权限拒绝的边界决定修复机制是否合适,需要用状态相关规则检验。

  • 业务语义校验
  • 可修复错误与权限拒绝

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    给任务JSON设计语法层与Schema层检查,举出能通过其中一层却不能通过另一层的输入,并说明你自己的错误分类。

    提交内容
    一份独立书面回答或标注图,包含自造例子、概念边界及判断依据。
    怎样算完成
    • 逐项回应题目要求,区分事实、假设与个人判断。
    • 能用自己的例子解释适用条件,记录尚未弄懂的点。

    练到的知识点JSON 语法校验 · Schema 结构校验

  2. 作业 02代码推演

    对缺失字段、null、空字符串和已填值四种任务记录,推演业务校验过程;明确required的约束对象和状态条件。

    提交内容
    手工推演记录,写明输入约定、中间状态、最终判断;可运行时附程序对照记录。
    怎样算完成
    • 推演包含中间过程,数据形状、单位、顺序或状态前后一致。
    • 主动检查至少一个边界或异常条件,能解释手算与运行差异。

    练到的知识点业务语义校验 · required 与 null

  3. 作业 03原理问答

    为学习计划操作定义状态相关规则:相同字段值在草稿、已确认、已取消状态下是否允许相同操作?列出判定所需事实与不可自动修复的情形。

    提交内容
    概念或合同说明、关系图与自造案例,明确事实、假设和适用条件。
    怎样算完成
    • 关键概念有明确边界,能用案例检验声明。
    • 未获证据支持的假设单独标注。

    练到的知识点业务语义校验 · 可修复错误与权限拒绝

综合应用

  1. 作业 04面试场景

    计划标题全为空白、时长为-5、附带未知字段。设计各项处理规则,说明拒绝、归一化与保留原输入的依据。

    提交内容
    场景分析与决策记录,包含信息缺口、约束、备选方案和验证计划。
    怎样算完成
    • 方案回应给定约束,列出至少一种替代方案及代价。
    • 每项关键结论说明所需证据,不能核实的部分明确标注。

    练到的知识点字符串空白及长度 · 数值范围与额外字段

  2. 作业 05编程实践

    实现校验流水线,Schema含default说明且输入缺字段;另外模拟无权限写操作,测试修复入口与权限判断不会被混成同一条路径。

    提交内容
    可复现的最小程序或实验材料、自写测试、环境说明与运行记录;题目允许纸面实验时提交完整案例表。
    怎样算完成
    • 提交物按说明可以重现;测试覆盖正常、边界与失败场景。
    • 运行证据与断言能核验声明的行为,注明模拟与真实执行范围。

    练到的知识点default 的说明性质 · 可修复错误与权限拒绝

  3. 作业 06排错实验

    建立一份边界输入表,覆盖极长字符串、数值边界、空集合和交叉字段冲突,逐项记录由哪一层发现问题。

    提交内容
    最小故障输入、复现步骤、观测记录、定位依据及回归用例。
    怎样算完成
    • 其他人可按记录复现故障,并区分现象与已验证原因。
    • 定位过程有观测依据,回归用例可以再次暴露原问题。

    练到的知识点Schema 结构校验 · 业务语义校验 · 字符串空白及长度

  4. 作业 07编程实践

    实现本地业务校验器,输入都通过JSON语法和Schema检查,但操作状态与用户权限各不相同;检查错误分类与修复入口是否符合合同。

    提交内容
    可复现的小程序或题目允许的离线实验、自写测试、输入和运行记录。
    怎样算完成
    • 输入、环境和运行过程可重现,测试覆盖声明的重点条件。
    • 验证目标由合同确定,模拟结果不冒充真实服务证据。

    练到的知识点业务语义校验 · 可修复错误与权限拒绝

  5. 作业 08排错实验

    在权限检查后改变目标记录的状态,再提交原先“可修复”的结果;记录语义验证使用的版本及检查后变更带来的未决问题。

    提交内容
    最小故障序列、观测记录、原因核对与可再次触发原问题的测试。
    怎样算完成
    • 故障可以稳定重现,现象与已验证原因分开。
    • 观测记录足以判断声明的合同是否被违反。

    练到的知识点业务语义校验 · 可修复错误与权限拒绝

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    为移动端旧客户端设计契约升级策略,区分新增必填、范围收紧与额外字段政策变化,并制定历史数据重放实验。

    提交内容
    设计评审文档或对照实验报告,包含前置条件、方案取舍、验收证据与尚未验证的范围。
    怎样算完成
    • 比较使用一致条件与事先声明的标准,记录失败与未知结果。
    • 说明方案的适用前提、未覆盖风险及后续验证方法。

    练到的知识点JSON 语法校验 · required 与 null · default 的说明性质

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 反例构造

    为“修复后的数据通过全部字段检查,就可以重试原操作”构造一个状态或权限改变的反例,给出判定需要的额外证据。

    提交内容
    具体反例、原主张的适用前提、复现或逻辑证明及结论边界。
    怎样算完成
    • 反例满足原主张所声称的前提,确实针对其结论。
    • 说明一个反例能推翻什么、不能进一步证明什么。

    练到的知识点业务语义校验 · 可修复错误与权限拒绝

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗