如何让Rust中JSON Schema验证错误按JSON字段顺序输出?
让JSON Schema验证错误按输入JSON字段顺序输出
问题背景
现有一段Rust代码用于验证输入JSON与JSON Schema的一致性,但遍历验证错误时,错误以随机顺序输出,与输入JSON的字段顺序不一致,希望调整为按输入JSON的字段顺序输出错误。
相关文件内容
abc.json
{ "performance": { "memory": 301, "cpu": 101, "threads": 4 } }
abc_schema.json
{ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "performance": { "type": "object", "description": "Performance parameters", "properties": { "memory": { "type": "number", "description": "Maximum memory usage", "minimum": 0, "maximum": 300 }, "cpu": { "type": "number", "description": "Maximum CPU usage", "minimum": 0, "maximum": 100 }, "threads": { "type": "number", "description": "Maximum threads usage", "minimum": 1, "maximum": 3 } }, "additionalProperties": false } }, "additionalProperties": false }
原Rust代码
use std::path::Path; use anyhow::{bail, Context, Result}; fn validate(content: &str, schema: &str) -> Result<()> { let content_json = serde_json::from_str(content).with_context(|| "Failed to parse JSON configuration")?; let schema_json = serde_json::from_str(schema).with_context(|| "Failed to parse JSON schema")?; let validator = jsonschema::validator_for(&schema_json) .with_context(|| "Invalid configuration JSON schema")?; let mut err_msg = String::from("JSON configuration format error(s):\n"); for error in validator.iter_errors(&content_json) { err_msg.push_str(format!(" - {error}").as_str()); err_msg.push_str(format!(" at {}\n", error.instance_path()).as_str()); } if !validator.is_valid(&content_json) { bail!(err_msg) } Ok(()) } fn main() { const ABC_CONFIG_SCHEMA: &str = include_str!(std::concat!( std::env!("CARGO_MANIFEST_DIR"), "/abc_schema.json" )); const ABC_CONFIG: &str = include_str!(std::concat!( std::env!("CARGO_MANIFEST_DIR"), "/abc.json" )); if let Err(err) = validate(ABC_CONFIG, ABC_CONFIG_SCHEMA) { eprintln!("{err:?}"); } }
Cargo.toml
[package] name = "json_schema_validator" version = "0.1.0" edition = "2024" [dependencies] anyhow = "1.0" jsonschema = { version = "0.46", default-features = false } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0"
当前输出
JSON configuration format error(s): - 101 is greater than the maximum of 100 at /performance/cpu - 301 is greater than the maximum of 300 at /performance/memory - 4 is greater than the maximum of 3 at /performance/threads
期望输出
JSON configuration format error(s): - 301 is greater than the maximum of 300 at /performance/memory - 101 is greater than the maximum of 100 at /performance/cpu - 4 is greater than the maximum of 3 at /performance/threads
解决方案
核心思路
错误顺序随机的原因:
- serde_json默认将JSON对象解析为无序的
HashMap,丢失了输入JSON的字段顺序; jsonschema库的错误遍历顺序依赖内部验证逻辑,不保证与输入字段顺序一致。
要实现按输入JSON字段顺序输出错误,需:
- 保留输入JSON的字段顺序;
- 按输入JSON的字段遍历顺序收集对应错误。
具体步骤
1. 修改Cargo.toml启用字段顺序保留
给serde_json添加preserve_order特性,让它解析JSON对象时使用有序映射:
serde_json = { version = "1.0", features = ["preserve_order"] }
2. 修改验证代码,按输入顺序收集错误
修改validate函数,先收集所有错误到哈希映射(路径到错误),再递归遍历输入JSON的结构,按顺序提取对应错误:
use std::collections::HashMap; use std::path::Path; use anyhow::{bail, Context, Result}; use serde_json::Value; // 递归遍历JSON Value,按字段顺序收集对应错误 fn collect_errors_in_order( value: &Value, current_path: &str, error_map: &HashMap<String, jsonschema::ValidationError>, errors: &mut Vec<jsonschema::ValidationError>, ) { match value { Value::Object(obj) => { for (key, val) in obj { let new_path = if current_path.is_empty() { format!("/{key}") } else { format!("{current_path}/{key}") }; // 如果当前路径有错误,加入列表 if let Some(err) = error_map.get(&new_path) { errors.push(err.clone()); } // 递归处理子字段 collect_errors_in_order(val, &new_path, error_map, errors); } } Value::Array(arr) => { for (idx, val) in arr.iter().enumerate() { let new_path = format!("{current_path}[{idx}]"); if let Some(err) = error_map.get(&new_path) { errors.push(err.clone()); } collect_errors_in_order(val, &new_path, error_map, errors); } } // 基础类型无需递归,直接检查当前路径是否有错误 _ => { if let Some(err) = error_map.get(current_path) { errors.push(err.clone()); } } } } fn validate(content: &str, schema: &str) -> Result<()> { // 解析输入JSON,保留字段顺序 let content_json = serde_json::from_str(content) .with_context(|| "Failed to parse JSON configuration")?; let schema_json = serde_json::from_str(schema) .with_context(|| "Failed to parse JSON schema")?; let validator = jsonschema::validator_for(&schema_json) .with_context(|| "Invalid configuration JSON schema")?; // 收集所有错误到哈希映射,键为实例路径 let mut error_map = HashMap::new(); for error in validator.iter_errors(&content_json) { error_map.insert(error.instance_path().to_string(), error); } if error_map.is_empty() { return Ok(()); } // 按输入JSON的字段顺序收集错误 let mut ordered_errors = Vec::new(); collect_errors_in_order(&content_json, "", &error_map, &mut ordered_errors); // 构建错误信息 let mut err_msg = String::from("JSON configuration format error(s):\n"); for error in ordered_errors { err_msg.push_str(&format!(" - {error} at {}\n", error.instance_path())); } bail!(err_msg) } fn main() { const ABC_CONFIG_SCHEMA: &str = include_str!(std::concat!( std::env!("CARGO_MANIFEST_DIR"), "/abc_schema.json" )); const ABC_CONFIG: &str = include_str!(std::concat!( std::env!("CARGO_MANIFEST_DIR"), "/abc.json" )); if let Err(err) = validate(ABC_CONFIG, ABC_CONFIG_SCHEMA) { eprintln!("{err}"); } }
说明
- 启用
preserve_order后,serde_json会将JSON对象解析为IndexMap,保留输入时的字段顺序; collect_errors_in_order函数递归遍历输入JSON的结构,按照字段出现的顺序从错误映射中取出对应错误,保证输出顺序与输入一致;- 最后按收集到的有序错误列表构建错误信息,输出即可得到期望的顺序。
内容的提问来源于stack exchange,提问作者Harry
相关产品推荐
相关产品推荐

