Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DRAFT] Provide richer error info for logical combination schemas #67

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/json-schema.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -119,20 +119,30 @@ extern json draft7_schema_builtin;
typedef std::function<void(const json_uri & /*id*/, json & /*value*/)> schema_loader;
typedef std::function<void(const std::string & /*format*/, const std::string & /*value*/)> format_checker;

// Validation error information
struct JSON_SCHEMA_VALIDATOR_API error_info
{
json::json_pointer ptr;
std::string message;
std::vector<error_info> 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
{
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;
}
Expand Down
128 changes: 68 additions & 60 deletions src/json-validator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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" });
}
};

Expand All @@ -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_; }
Expand All @@ -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:
Expand All @@ -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<error_info> 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<error_info> &) const {}

public:
logical_combination(json &sch,
Expand All @@ -319,25 +311,18 @@ template <>
const std::string logical_combination<oneOf>::key = "oneOf";

template <>
bool logical_combination<allOf>::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<anyOf>::is_validate_complete(const json &, const json::json_pointer &, error_handler &, const first_error_handler &, size_t count)
void logical_combination<allOf>::validate_combination_of_errors(const json::json_pointer &ptr, error_handler &e, const std::vector<error_info> &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<oneOf>::is_validate_complete(const json &instance, const json::json_pointer &ptr, error_handler &e, const first_error_handler &, size_t count)
void logical_combination<oneOf>::validate_combination_of_errors(const json::json_pointer &ptr, error_handler &e, const std::vector<error_info> &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
Expand All @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -542,32 +527,32 @@ 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() });
}
}

if (maxLength_.first) {
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<std::string>(), 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() });
}
}
}
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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" });
}
}

Expand All @@ -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:
Expand All @@ -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()) {
Expand All @@ -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 });
}
}

Expand Down Expand Up @@ -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" });
}
}

Expand Down Expand Up @@ -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'" });
}
}

Expand Down Expand Up @@ -1103,9 +1088,9 @@ std::shared_ptr<schema> 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");
}
};

Expand All @@ -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<root_schema>(new root_schema(loader, format)))
Expand Down