使用 std::variant 和 std::visit 实现类型安全的 schema 校验,强制覆盖所有类型分支,避免 std::bad_variant_access;配合 nlohmann::json 归一化数值类型、递归校验嵌套结构,并用 std::expected 或状态码聚合错误。

用 std::variant 和 std::visit 做类型安全的 schema 检查
直接硬编码 if-else 判断字段类型容易漏分支、难维护,也扛不住新增类型。C++17 的 std::variant 配合 std::visit 是更稳的选择——它强制你处理所有可能类型,编译期就能拦住没覆盖的 case。
常见错误现象:std::get<int>(val)</int> 在 val 实际是 double 时抛 std::bad_variant_access;不加 std::visit 而用多次 std::holds_alternative 易写漏或顺序错。
- 把配置值统一存为
std::variant<int double std::string bool std::nullptr_t></int>,null 表示缺失 - schema 定义用 struct,比如
struct FieldSchema { std::string type; bool required = false; std::optional<int> min; };</int> - 校验函数接收
const std::variant<...>&</...>和const FieldSchema&,内部用std::visit分发到具体类型处理器 - 别在 visit lambda 里 throw 异常来中断流程——改用返回
std::expected<void std::string></void>(C++23)或自定义状态码,方便聚合多个错误
JSON 解析后怎么映射到你的 schema 结构?
用 nlohmann::json 是最省事的路径,但它默认把整数、浮点都转成 json::number_float_t 或 json::number_integer_t,导致你写的 std::holds_alternative<int></int> 总是 false。
使用场景:读取 config.json 后要验证 "timeout" 字段是否为正整数,但原始 JSON 里它可能是 30(int)、30.0(float),甚至字符串 "30"。
立即学习“C++免费学习笔记(深入)”;
- 解析后立刻做一次“类型归一化”:对每个字段调用
json::is_number_integer()/is_number_float()/is_string()判断,再用json::get<int>()</int>等显式提取,塞进你的std::variant - 字符串转数字要自己处理:若 schema 要求 int,但 JSON 给的是
"42",得调std::stoi并捕获异常,不能依赖json::get<int>()</int> - 避免直接用
json::dump()输出调试——它会把null打印成null,但你的std::variant里是std::nullptr_t,类型不等价
嵌套对象和数组怎么递归校验?
平铺字段好办,但 "database": {"host": "localhost", "port": 5432} 这种结构,必须支持 schema 描述子结构,否则只能写死逻辑。
性能影响:每层递归都拷贝 nlohmann::json 对象会触发深拷贝,大配置下明显变慢。
- schema 中用
std::optional<:map fieldschema>> object_schema</:map>描述对象字段约束 - 校验函数接受
const nlohmann::json&引用,而不是值传递,避免无谓拷贝 - 数组校验不要只检查长度,要传入元素级的
FieldSchema,然后遍历每个元素调用同一套校验函数——复用比重写安全 - 递归深度超 10 层时,建议加个计数器提前报错,防止栈溢出或恶意构造的深层嵌套 JSON
为什么不用第三方 validation 库?
像 json-schema-validator 功能全,但链接体积涨 2MB+,启动时加载 schema 编译成 AST 有延迟,且错误信息格式固定、不好嵌入你自己的日志上下文。
兼容性影响:有些嵌入式环境不支持 C++17,或禁用 RTTI(而 std::variant 依赖它);这时得退回到 union + 手动 tag 枚举,但必须严格配对 switch 分支和类型操作。
- 如果项目已用
nlohmann::json,且 schema 规则简单(非完整 JSON Schema v7),手写验证器几小时就能跑通核心路径 - 关键是要把“字段名 → 类型/范围/必填”这些规则从代码里抽出来,哪怕只是
std::map<:string fieldschema></:string>,别散落在 if 分支里 - 最容易被忽略的是空字符串
""和null的语义区分——很多 API 把空字符串当有效值,但你的业务可能要求非空,这必须在字符串分支里单独 check.empty()










