From e92770989133235fdc45a5b42b2c45e0845ec58c Mon Sep 17 00:00:00 2001 From: Daniel Zint Date: Tue, 4 Aug 2026 14:53:41 +0200 Subject: [PATCH 1/2] Improve the error message when an input entry lacks a rule. --- src/jse/jse.cpp | 50 +++++++++++++++++++++++++-------- src/jse/jse.h | 4 +++ tests/test_validator.cpp | 60 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 11 deletions(-) diff --git a/src/jse/jse.cpp b/src/jse/jse.cpp index e7f63d2..1e4c8f0 100644 --- a/src/jse/jse.cpp +++ b/src/jse/jse.cpp @@ -184,6 +184,25 @@ namespace jse return true; } + // If there is only one candidate rule (no ambiguity) and it is an object rule + // that failed only because of extra/unknown fields, pinpoint those fields + // instead of dumping the whole subtree. + if (strict && matching_rules.size() == 1 && matching_rules[0].value("type", "") == "object") + { + std::vector extra_keys = find_extra_keys(input, matching_rules[0]); + if (!extra_keys.empty()) + { + for (const std::string &key : extra_keys) + { + const string extra_pointer = append_pointer(pointer, key); + std::stringstream s; + s << "No rule matched for \"" << extra_pointer << "\": " << input[key].dump(/*indent=*/4); + log.push_back(log_item("error", s.str())); + } + return false; + } + } + std::stringstream s; s << "No rule matched for \"" << pointer << "\": " << input.dump(/*indent=*/4) << std::endl; s << "No valid rules in this list:\n"; @@ -414,6 +433,24 @@ namespace jse return true; } + std::vector JSE::find_extra_keys(const json &input, const json &rule) + { + std::vector keys; + keys.reserve((rule.contains("required") ? rule["required"].size() : 0) + + (rule.contains("optional") ? rule["optional"].size() : 0)); + if (rule.contains("required")) + keys.insert(keys.end(), rule["required"].begin(), rule["required"].end()); + if (rule.contains("optional")) + keys.insert(keys.end(), rule["optional"].begin(), rule["optional"].end()); + + std::vector extra; + if (input.is_object()) + for (const auto &[key, value] : input.items()) + if (std::find(keys.begin(), keys.end(), key) == keys.end()) + extra.push_back(key); + return extra; + } + bool JSE::verify_rule_object(const json &input, const json &rule) { assert(rule.at("type") == "object"); @@ -429,17 +466,8 @@ namespace jse if (strict) // strict mode: check that no extra fields are present { - std::vector keys; - keys.reserve((rule.contains("required") ? rule["required"].size() : 0) - + (rule.contains("optional") ? rule["optional"].size() : 0)); - if (rule.contains("required")) - keys.insert(keys.end(), rule["required"].begin(), rule["required"].end()); - if (rule.contains("optional")) - keys.insert(keys.end(), rule["optional"].begin(), rule["optional"].end()); - - for (const auto &[key, value] : input.items()) - if (std::find(keys.begin(), keys.end(), key) == keys.end()) - return false; + if (!find_extra_keys(input, rule).empty()) + return false; } if (rule.contains("type_name") diff --git a/src/jse/jse.h b/src/jse/jse.h index 81752e1..2a07887 100644 --- a/src/jse/jse.h +++ b/src/jse/jse.h @@ -77,6 +77,10 @@ namespace jse // Find the first rule matching a pointer json find_valid_rule(const string &pointer, const json &input, const json &rules); + // Returns the keys of input that are not listed in rule's "required" or "optional" lists. + // Only meaningful for rules of type "object". + std::vector find_extra_keys(const json &input, const json &rule); + // Utils bool contained_in_list(string item, const json &list); diff --git a/tests/test_validator.cpp b/tests/test_validator.cpp index 2129714..09e064d 100644 --- a/tests/test_validator.cpp +++ b/tests/test_validator.cpp @@ -221,6 +221,66 @@ TEST_CASE("type_object", "[validator]") REQUIRE(!jse.verify_json(input, rules)); } +TEST_CASE("strict_unknown_parameter", "[validator]") +{ + json input = R"( + { + "string1": "teststring" + } + )"_json; + + json rules = R"( + [ + { + "pointer": "/", + "type": "object", + "required": ["string1"] + } + ] + )"_json; + + JSE jse; + + jse.strict = true; + REQUIRE(!jse.verify_json(input, rules)); + REQUIRE(jse.log2str().find("Unknown entry /string1") != std::string::npos); + + jse.strict = false; + REQUIRE(jse.verify_json(input, rules)); +} + +TEST_CASE("strict_unlisted_extra_parameter", "[validator]") +{ + // "skip_simplif" is a typo for "skip_simplify" and is not listed in + // required/optional, so it should be pinpointed rather than triggering + // a dump of the whole object. + json input = R"( + { + "string1": "teststring", + "foo2": false + } + )"_json; + + json rules = R"( + [ + { + "pointer": "/", + "type": "object", + "required": ["string1"], + "optional": ["foo1"] + } + ] + )"_json; + + JSE jse; + + jse.strict = true; + REQUIRE(!jse.verify_json(input, rules)); + std::cout << jse.log2str() << std::endl; + REQUIRE(jse.log2str().find("No rule matched for \"/foo2\": false") != std::string::npos); + REQUIRE(jse.log2str().find("No valid rules in this list") == std::string::npos); +} + TEST_CASE("include_rule", "[validator]") { json rules = R"( From 87174a3685eae33e3b6ce8e3431cbdb05813feab Mon Sep 17 00:00:00 2001 From: Daniel Zint Date: Tue, 4 Aug 2026 15:00:49 +0200 Subject: [PATCH 2/2] Write list of rules as well. --- src/jse/jse.cpp | 5 ++++- tests/test_validator.cpp | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/jse/jse.cpp b/src/jse/jse.cpp index 1e4c8f0..ac50c77 100644 --- a/src/jse/jse.cpp +++ b/src/jse/jse.cpp @@ -196,7 +196,10 @@ namespace jse { const string extra_pointer = append_pointer(pointer, key); std::stringstream s; - s << "No rule matched for \"" << extra_pointer << "\": " << input[key].dump(/*indent=*/4); + s << "No rule matched for \"" << extra_pointer << "\": " << input[key].dump(/*indent=*/4) << std::endl; + s << "No valid rules in this list:\n"; + for (int i = 0; i < matching_rules.size(); i++) + s << i << ": " << matching_rules[i].dump(/*indent=*/4) << "\n"; log.push_back(log_item("error", s.str())); } return false; diff --git a/tests/test_validator.cpp b/tests/test_validator.cpp index 09e064d..5711261 100644 --- a/tests/test_validator.cpp +++ b/tests/test_validator.cpp @@ -276,9 +276,8 @@ TEST_CASE("strict_unlisted_extra_parameter", "[validator]") jse.strict = true; REQUIRE(!jse.verify_json(input, rules)); - std::cout << jse.log2str() << std::endl; REQUIRE(jse.log2str().find("No rule matched for \"/foo2\": false") != std::string::npos); - REQUIRE(jse.log2str().find("No valid rules in this list") == std::string::npos); + REQUIRE(jse.log2str().find("No valid rules in this list") != std::string::npos); } TEST_CASE("include_rule", "[validator]")