You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让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

解决方案

核心思路

错误顺序随机的原因:

  1. serde_json默认将JSON对象解析为无序的HashMap,丢失了输入JSON的字段顺序;
  2. jsonschema库的错误遍历顺序依赖内部验证逻辑,不保证与输入字段顺序一致。

要实现按输入JSON字段顺序输出错误,需:

  1. 保留输入JSON的字段顺序;
  2. 按输入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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.01 18:24:52