阅读文本 / Reading text
本文使用合成样本演示处理方法。代码已通过隔离环境中的示例检查;这不代表对所有真实文件或产品效果的保证。处理重要文件前,请保留原件,并按文中的边界与验收项复核。
一个接口同时接收发票和收据,很容易写出“所有字段都可选”的大对象。这样的对象什么都能装,却很难说明收到的究竟是什么。更清楚的做法是保留两份独立契约,用明确类型字段选择其中一份。
oneOf 不是任意一个就行
oneOf
要求恰好匹配一个分支。两个分支同时通过也算失败。若分支只有几个可选字段,它们可能大量重叠,合法输入反而无法通过。用必填的
kind
加
const
,能把这类重叠提前消除。
一个可执行的分流协议
from jsonschema import Draft202012Validator
def branch(kind, identity):
return {
"type": "object", "additionalProperties": False,
"required": ["kind", identity],
"properties": {"kind": {"const": kind},
identity: {"type": "string", "minLength": 1}}
}
schema = {"oneOf": [branch("invoice", "invoice_number"),
branch("receipt", "receipt_number")]}
v = Draft202012Validator(schema)
valid = lambda x: not list(v.iter_errors(x))
assert valid({"kind": "invoice", "invoice_number": "I-1"})
assert valid({"kind": "receipt", "receipt_number": "R-1"})
assert not valid({"kind": "invoice", "receipt_number": "R-1"})
assert not valid({"kind": "receipt", "receipt_number": ""})
assert not valid({"kind": "invoice", "invoice_number": "I-1",
"receipt_number": "R-1"})
assert not valid({"kind": "unknown"})
这是人工构造的最小记录,不是现实发票的完整字段模型。代码演示分支选择与多余字段拒绝,税率、抬头和金额需要另行定义。业务若允许附加字段,也应明确它们的命名空间,而不是悄悄取消所有约束。
错误信息别只显示“不匹配”
输入特征
实际疑点
建议反馈
kind 缺失
无法选择协议
请提供单据类型
类型与编号字段冲突
两种结构被混合
展示冲突字段及来源
编号空字符串
键存在但无值
回看编号区域
未知类型
当前协议未覆盖
进入独立待分类队列
不确定类型也需要一个出口
如果分类器无法区分发票与收据,应保留未知状态,而不是随机选一个分支凑出“成功”。分类置信度、结构校验和业务真实性是三个不同层次。结构通过不能反向证明分类正确,更不能证明单据真实。
翻译流程里也要保留单据类型
同一字段在不同单据中可能需要不同术语。使用
OmniDoc(产品入口)
的术语与双语审查工作流时,把单据类型随交付清单一起提供,有助于审校理解编号和金额所处的语境。不要让一个宽泛 JSON 对象抹掉这些差别。
参考:
JSON Schema 组合规则
。
- 1 -
更新于 2026-10-08