diff --git a/src/jse/jse.cpp b/src/jse/jse.cpp index e7f63d2..ac50c77 100644 --- a/src/jse/jse.cpp +++ b/src/jse/jse.cpp @@ -184,6 +184,28 @@ 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) << 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; + } + } + 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 +436,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 +469,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..5711261 100644 --- a/tests/test_validator.cpp +++ b/tests/test_validator.cpp @@ -221,6 +221,65 @@ 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)); + 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"(