From bbaca7ea2b06058df6247bbcee5536e246cda0b9 Mon Sep 17 00:00:00 2001 From: garethsb-sony Date: Thu, 15 Aug 2019 17:15:57 +0100 Subject: [PATCH] First stab at providing rich error info for logical combination schemas (allOf, anyOf, oneOf) --- src/json-schema.hpp | 14 ++++- src/json-validator.cpp | 128 ++++++++++++++++++++++------------------- 2 files changed, 80 insertions(+), 62 deletions(-) diff --git a/src/json-schema.hpp b/src/json-schema.hpp index 165a9c9..a2f2a32 100644 --- a/src/json-schema.hpp +++ b/src/json-schema.hpp @@ -119,12 +119,22 @@ extern json draft7_schema_builtin; typedef std::function schema_loader; typedef std::function format_checker; +// Validation error information +struct JSON_SCHEMA_VALIDATOR_API error_info +{ + json::json_pointer ptr; + std::string message; + std::vector subschema_errors; + // formatted error information including ptr, message and subschema errors + std::string formatted() const; +}; + // Interface for validation error handlers class JSON_SCHEMA_VALIDATOR_API error_handler { public: virtual ~error_handler() {} - virtual void error(const json::json_pointer & /*ptr*/, const json & /*instance*/, const std::string & /*message*/) = 0; + virtual void error(const error_info & /*err*/) = 0; }; class JSON_SCHEMA_VALIDATOR_API basic_error_handler : public error_handler @@ -132,7 +142,7 @@ class JSON_SCHEMA_VALIDATOR_API basic_error_handler : public error_handler bool error_{false}; public: - void error(const json::json_pointer & /*ptr*/, const json & /*instance*/, const std::string & /*message*/) override + void error(const error_info & /*err*/) override { error_ = true; } diff --git a/src/json-validator.cpp b/src/json-validator.cpp index 5db8b23..73ea60a 100644 --- a/src/json-validator.cpp +++ b/src/json-validator.cpp @@ -57,7 +57,7 @@ class schema_ref : public schema if (target_) target_->validate(ptr, instance, e); else - e.error(ptr, instance, "unresolved schema-reference " + id_); + e.error({ ptr, "unresolved schema-reference " + id_ }); } public: @@ -207,7 +207,7 @@ class root_schema : public schema if (root_) root_->validate(ptr, instance, e); else - e.error(ptr, "", "no root schema has yet been set for validating an instance"); + e.error({ ptr, "no root schema has yet been set for validating an instance" }); } }; @@ -221,18 +221,14 @@ class first_error_handler : public error_handler { public: bool error_{false}; - json::json_pointer ptr_; - json instance_; - std::string message_; + error_info err_; - void error(const json::json_pointer & ptr, const json & instance, const std::string & message) override + void error(const error_info & err) override { if (*this) return; error_ = true; - ptr_ = ptr; - instance_ = instance; - message_ = message; + err_ = err; } operator bool() const { return error_; } @@ -248,7 +244,7 @@ class logical_not : public schema subschema_->validate(ptr, instance, esub); if (!esub) - e.error(ptr, instance, "the subschema has succeeded, but it is required to not validate"); + e.error({ ptr, "the subschema has succeeded, but it is required to not validate" }); } public: @@ -274,27 +270,23 @@ class logical_combination : public schema void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const final { - size_t count = 0; + std::vector subschemata_errors; for (auto &s : subschemata_) { first_error_handler esub; s->validate(ptr, instance, esub); - if (!esub) - count++; - - if (is_validate_complete(instance, ptr, e, esub, count)) - return; + if (esub) + subschemata_errors.push_back(esub.err_); } - // could accumulate esub details for anyOf and oneOf, but not clear how to select which subschema failure to report - // or how to report multiple such failures - if (count == 0) - e.error(ptr, instance, "no subschema has succeeded, but one of them is required to validate"); + if (subschemata_.size() == subschemata_errors.size()) + e.error({ ptr, "no subschema has succeeded, but at least one of them is required to validate", std::move(subschemata_errors) }); + validate_combination_of_errors(ptr, e, subschemata_errors); } // specialized for each of the logical_combination_types static const std::string key; - static bool is_validate_complete(const json &, const json::json_pointer &, error_handler &, const first_error_handler &, size_t); + void validate_combination_of_errors(const json::json_pointer &, error_handler &, const std::vector &) const {} public: logical_combination(json &sch, @@ -319,25 +311,18 @@ template <> const std::string logical_combination::key = "oneOf"; template <> -bool logical_combination::is_validate_complete(const json &, const json::json_pointer &, error_handler &e, const first_error_handler &esub, size_t) -{ - if (esub) - e.error(esub.ptr_, esub.instance_, "at least one subschema has failed, but all of them are required to validate - " + esub.message_); - return esub; -} - -template <> -bool logical_combination::is_validate_complete(const json &, const json::json_pointer &, error_handler &, const first_error_handler &, size_t count) +void logical_combination::validate_combination_of_errors(const json::json_pointer &ptr, error_handler &e, const std::vector &subschemata_errors) const { - return count == 1; + if (!subschemata_errors.empty()) + e.error({ ptr, "at least one subschema has failed, but all of them are required to validate", std::move(subschemata_errors) }); } template <> -bool logical_combination::is_validate_complete(const json &instance, const json::json_pointer &ptr, error_handler &e, const first_error_handler &, size_t count) +void logical_combination::validate_combination_of_errors(const json::json_pointer &ptr, error_handler &e, const std::vector &subschemata_errors) const { - if (count > 1) - e.error(ptr, instance, "more than one subschema has succeeded, but exactly one of them is required to validate"); - return count > 1; + // hmm, the most useful information would be the indices of the subschemata that succeeded + if (subschemata_errors.size() < subschemata_.size() - 1) + e.error({ ptr, "more than one subschema has succeeded, but exactly one of them is required to validate", std::move(subschemata_errors) }); } class type_schema : public schema @@ -362,7 +347,7 @@ class type_schema : public schema if (type) type->validate(ptr, instance, e); else - e.error(ptr, instance, "unexpected instance type"); + e.error({ ptr, "unexpected instance type" }); if (enum_.first) { bool seen_in_enum = false; @@ -373,12 +358,12 @@ class type_schema : public schema } if (!seen_in_enum) - e.error(ptr, instance, "instance not found in required enum"); + e.error({ ptr, "instance not found in required enum" }); } if (const_.first && const_.second != instance) - e.error(ptr, instance, "instance not const"); + e.error({ ptr, "instance not const" }); for (auto l : logic_) l->validate(ptr, instance, e); @@ -542,7 +527,7 @@ class string : public schema if (utf8_length(instance) < minLength_.second) { std::ostringstream s; s << "instance is too short as per minLength:" << minLength_.second; - e.error(ptr, instance, s.str()); + e.error({ ptr, s.str() }); } } @@ -550,24 +535,24 @@ class string : public schema if (utf8_length(instance) > maxLength_.second) { std::ostringstream s; s << "instance is too long as per maxLength: " << maxLength_.second; - e.error(ptr, instance, s.str()); + e.error({ ptr, s.str() }); } } #ifndef NO_STD_REGEX if (pattern_.first && !REGEX_NAMESPACE::regex_search(instance.get(), pattern_.second)) - e.error(ptr, instance, "instance does not match regex pattern: " + patternString_); + e.error({ ptr, "instance does not match regex pattern: " + patternString_ }); #endif if (format_.first) { if (root_->format_check() == nullptr) - e.error(ptr, instance, std::string("a format checker was not provided but a format keyword for this string is present: ") + format_.second); + e.error({ ptr, std::string("a format checker was not provided but a format keyword for this string is present: ") + format_.second }); else { try { root_->format_check()(format_.second, instance); } catch (const std::exception &ex) { - e.error(ptr, instance, std::string("format-checking failed: ") + ex.what()); + e.error({ ptr, std::string("format-checking failed: ") + ex.what() }); } } } @@ -632,17 +617,17 @@ class numeric : public schema if (multipleOf_.first && value != 0) // zero is multiple of everything if (violates_multiple_of(value)) - e.error(ptr, instance, "instance is not a multiple of " + std::to_string(multipleOf_.second)); + e.error({ ptr, "instance is not a multiple of " + std::to_string(multipleOf_.second) }); if (maximum_.first) if ((exclusiveMaximum_ && value >= maximum_.second) || value > maximum_.second) - e.error(ptr, instance, "instance exceeds maximum of " + std::to_string(maximum_.second)); + e.error({ ptr, "instance exceeds maximum of " + std::to_string(maximum_.second) }); if (minimum_.first) if ((exclusiveMinimum_ && value <= minimum_.second) || value < minimum_.second) - e.error(ptr, instance, "instance is below minimum of " + std::to_string(minimum_.second)); + e.error({ ptr, "instance is below minimum of " + std::to_string(minimum_.second) }); } public: @@ -688,7 +673,7 @@ class null : public schema void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override { if (!instance.is_null()) - e.error(ptr, instance, "expected to be null"); + e.error({ ptr, "expected to be null" }); } public: @@ -715,11 +700,11 @@ class boolean : public schema //switch (instance.type()) { //case json::value_t::array: // if (instance.size() != 0) // valid false-schema - // e.error(ptr, instance, "false-schema required empty array"); + // e.error({ ptr, "false-schema required empty array" }); // return; //} - e.error(ptr, instance, "instance invalid as per false-schema"); + e.error({ ptr, "instance invalid as per false-schema" }); } } @@ -736,7 +721,7 @@ class required : public schema { for (auto &r : required_) if (instance.find(r) == instance.end()) - e.error(ptr, instance, "required property '" + r + "' not found in object as a dependency"); + e.error({ ptr, "required property '" + r + "' not found in object as a dependency" }); } public: @@ -763,14 +748,14 @@ class object : public schema void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override { if (maxProperties_.first && instance.size() > maxProperties_.second) - e.error(ptr, instance, "too many properties"); + e.error({ ptr, "too many properties" }); if (minProperties_.first && instance.size() < minProperties_.second) - e.error(ptr, instance, "too few properties"); + e.error({ ptr, "too few properties" }); for (auto &r : required_) if (instance.find(r) == instance.end()) - e.error(ptr, instance, "required property '" + r + "' not found in object"); + e.error({ ptr, "required property '" + r + "' not found in object" }); // for each property in instance for (auto &p : instance.items()) { @@ -797,7 +782,7 @@ class object : public schema first_error_handler additional_prop_err; additionalProperties_->validate(ptr / p.key(), p.value(), additional_prop_err); if (additional_prop_err) - e.error(ptr, instance, "validation failed for additional property '" + p.key() + "': " + additional_prop_err.message_); + e.error({ ptr, "validation failed for additional property '" + p.key() + "': " + additional_prop_err.err_.message }); } } @@ -902,16 +887,16 @@ class array : public schema void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override { if (maxItems_.first && instance.size() > maxItems_.second) - e.error(ptr, instance, "array has too many items"); + e.error({ ptr, "array has too many items" }); if (minItems_.first && instance.size() < minItems_.second) - e.error(ptr, instance, "array has too few items"); + e.error({ ptr, "array has too few items" }); if (uniqueItems_) { for (auto it = instance.cbegin(); it != instance.cend(); ++it) { auto v = std::find(it + 1, instance.end(), *it); if (v != instance.end()) - e.error(ptr, instance, "items have to be unique for this array"); + e.error({ ptr, "items have to be unique for this array" }); } } @@ -950,7 +935,7 @@ class array : public schema } } if (!contained) - e.error(ptr, instance, "array does not contain required element as per 'contains'"); + e.error({ ptr, "array does not contain required element as per 'contains'" }); } } @@ -1103,9 +1088,9 @@ std::shared_ptr schema::make(json &schema, class throwing_error_handler : public error_handler { - void error(const json::json_pointer &ptr, const json &instance, const std::string &message) override + void error(const error_info &err) override { - throw std::invalid_argument(std::string("At ") + ptr.to_string() + " of " + instance.dump() + " - " + message + "\n"); + throw std::invalid_argument(std::string("At ") + err.ptr.to_string() + " - " + err.message + "\n"); } }; @@ -1116,6 +1101,29 @@ namespace nlohmann namespace json_schema { +namespace +{ + void do_format(std::ostream &s, const error_info &err, size_t level = 0) + { + s << (nlohmann::json::json_pointer() == err.ptr ? "root" : err.ptr.to_string()) << " - " << err.message; + if (err.subschema_errors.empty()) return; + // this would benefit from an option to use a heuristic to only output the best match + ++level; + for (auto& sub : err.subschema_errors) + { + s << '\n' << std::string(level, '\t'); + do_format(s, sub, level); + } + } +} + +std::string error_info::formatted() const +{ + std::ostringstream s; + do_format(s, *this); + return s.str(); +} + json_validator::json_validator(schema_loader loader, format_checker format) : root_(std::unique_ptr(new root_schema(loader, format)))