From 0dca07bdf826cbd5e6d1c5fdbcf5559a6b9cdfa5 Mon Sep 17 00:00:00 2001 From: Sven Fink Date: Tue, 3 Mar 2020 16:09:17 +0100 Subject: [PATCH] Take care of default values for validation validation may depend on default values in case of "if, ..." statements. Therefore, validation operates on a copy and adds default values. In alternative, it serves a validate_and_fill which avoids a copy. --- CMakeLists.txt | 1 - src/json-patch.cpp | 78 -------------------------- src/json-patch.hpp | 38 ------------- src/json-validator.cpp | 94 ++++++++++++++++++-------------- src/nlohmann/json-schema.hpp | 6 ++ test/issue-25-default-values.cpp | 39 ++++++++++++- 6 files changed, 97 insertions(+), 159 deletions(-) delete mode 100644 src/json-patch.cpp delete mode 100644 src/json-patch.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c4cc218..5c809d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,6 @@ add_library(nlohmann_json_schema_validator src/json-schema-draft7.json.cpp src/json-uri.cpp src/json-validator.cpp - src/json-patch.cpp src/string-format-check.cpp) target_include_directories(nlohmann_json_schema_validator diff --git a/src/json-patch.cpp b/src/json-patch.cpp deleted file mode 100644 index e55b167..0000000 --- a/src/json-patch.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "json-patch.hpp" - -namespace nlohmann -{ - -json_patch::json_patch() - : j_{R"([])"_json} {} - -json_patch::json_patch(json &&patch) - : j_{std::move(patch)} -{ - validateJsonPatch(j_); -} - -json_patch::json_patch(const json &patch) - : j_{std::move(patch)} -{ - validateJsonPatch(j_); -} - -json_patch &json_patch::add(std::string path, json value) -{ - j_.push_back(json{{"op", "add"}, {"path", std::move(path)}, {"value", std::move(value)}}); - return *this; -} - -json_patch &json_patch::replace(std::string path, json value) -{ - j_.push_back(json{{"op", "replace"}, {"path", std::move(path)}, {"value", std::move(value)}}); - return *this; -} - -json_patch &json_patch::remove(std::string path) -{ - j_.push_back(json{{"op", "remove"}, {"path", std::move(path)}}); - return *this; -} - -void json_patch::validateJsonPatch(json const &patch) -{ - if (!patch.is_array()) { - throw JsonPatchFormatException{"Json is not an array"}; - } - - for (auto const &op : patch) { - if (!op.is_object()) { - throw JsonPatchFormatException{"Each json patch entry needs to be an op object"}; - } - - if (!op.contains("op")) { - throw JsonPatchFormatException{"Each json patch entry needs op element"}; - } - - const auto opType = op["op"].get(); - if ((opType != "add") && (opType != "remove") && (opType != "replace")) { - throw JsonPatchFormatException{std::string{"Operation "} + opType + std::string{"is invalid"}}; - } - - if (!op.contains("path")) { - throw JsonPatchFormatException{"Each json patch entry needs path element"}; - } - - try { - // try parse to path - [[maybe_unused]] const auto p = json::json_pointer{op["path"].get()}; - } catch (json::exception &e) { - throw JsonPatchFormatException{e.what()}; - } - - if (opType != "remove") { - if (!op.contains("value")) { - throw JsonPatchFormatException{"Remove and replace needs value element"}; - } - } - } -} - -} // namespace nlohmann diff --git a/src/json-patch.hpp b/src/json-patch.hpp deleted file mode 100644 index 6453297..0000000 --- a/src/json-patch.hpp +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include -#include - -namespace nlohmann -{ -class JsonPatchFormatException : public std::exception -{ -public: - explicit JsonPatchFormatException(std::string msg) - : ex_{std::move(msg)} {} - - inline const char *what() const noexcept override final { return ex_.c_str(); } - -private: - std::string ex_; -}; - -class json_patch -{ -public: - json_patch(); - json_patch(json &&patch); - json_patch(const json &patch); - - json_patch &add(std::string path, json value); - json_patch &replace(std::string path, json value); - json_patch &remove(std::string path); - - operator json() const { return j_; } - -private: - json j_; - - static void validateJsonPatch(json const &patch); -}; -} // namespace nlohmann diff --git a/src/json-validator.cpp b/src/json-validator.cpp index c684578..9b8ab13 100644 --- a/src/json-validator.cpp +++ b/src/json-validator.cpp @@ -8,14 +8,12 @@ */ #include -#include "json-patch.hpp" - +#include #include #include #include using nlohmann::json; -using nlohmann::json_patch; using nlohmann::json_uri; using nlohmann::json_schema::root_schema; using namespace nlohmann::json_schema; @@ -46,7 +44,7 @@ public: schema(root_schema *root) : root_(root) {} - virtual void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const = 0; + virtual void validate(const json::json_pointer &ptr, json &instance, error_handler &e) const = 0; virtual const json &defaultValue(const json::json_pointer &, const json &, error_handler &) const { @@ -64,10 +62,10 @@ class schema_ref : public schema const std::string id_; std::shared_ptr target_; - void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final + void validate(const json::json_pointer &ptr, json &instance, error_handler &e) const final { if (target_) - target_->validate(ptr, instance, patch, e); + target_->validate(ptr, instance, e); else e.error(ptr, instance, "unresolved schema-reference " + id_); } @@ -230,10 +228,10 @@ public: } while (1); } - void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final + void validate(const json::json_pointer &ptr, json &instance, error_handler &e) const final { if (root_) - root_->validate(ptr, instance, patch, e); + root_->validate(ptr, instance, e); else e.error(ptr, "", "no root schema has yet been set for validating an instance"); } @@ -280,10 +278,10 @@ class logical_not : public schema { std::shared_ptr subschema_; - void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final + void validate(const json::json_pointer &ptr, json &instance, error_handler &e) const final { first_error_handler esub; - subschema_->validate(ptr, instance, patch, esub); + subschema_->validate(ptr, instance, esub); if (!esub) e.error(ptr, instance, "the subschema has succeeded, but it is required to not validate"); @@ -315,13 +313,13 @@ class logical_combination : public schema { std::vector> subschemata_; - void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final + void validate(const json::json_pointer &ptr, json &instance, error_handler &e) const final { size_t count = 0; for (auto &s : subschemata_) { first_error_handler esub; - s->validate(ptr, instance, patch, esub); + s->validate(ptr, instance, esub); if (!esub) count++; @@ -403,13 +401,13 @@ class type_schema : public schema return defaultValue_; } - void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const override final + void validate(const json::json_pointer &ptr, json &instance, error_handler &e) const override final { // depending on the type of instance run the type specific validator - if present auto type = type_[(uint8_t) instance.type()]; if (type) - type->validate(ptr, instance, patch, e); + type->validate(ptr, instance, e); else e.error(ptr, instance, "unexpected instance type"); @@ -430,18 +428,18 @@ class type_schema : public schema e.error(ptr, instance, "instance not const"); for (auto l : logic_) - l->validate(ptr, instance, patch, e); + l->validate(ptr, instance, e); if (if_) { first_error_handler err; - if_->validate(ptr, instance, patch, err); + if_->validate(ptr, instance, err); if (!err) { if (then_) - then_->validate(ptr, instance, patch, e); + then_->validate(ptr, instance, e); } else { if (else_) - else_->validate(ptr, instance, patch, e); + else_->validate(ptr, instance, e); } } } @@ -590,7 +588,7 @@ class string : public schema return len; } - void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override + void validate(const json::json_pointer &ptr, json &instance, error_handler &e) const override { if (minLength_.first) { if (utf8_length(instance) < minLength_.second) { @@ -680,7 +678,7 @@ class numeric : public schema return std::fabs(res) > std::fabs(eps); } - void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override + void validate(const json::json_pointer &ptr, json &instance, error_handler &e) const override { T value = instance; // conversion of json to value_type @@ -739,7 +737,7 @@ public: class null : public schema { - void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override + void validate(const json::json_pointer &ptr, json &instance, error_handler &e) const override { if (!instance.is_null()) e.error(ptr, instance, "expected to be null"); @@ -752,7 +750,7 @@ public: class boolean_type : public schema { - void validate(const json::json_pointer &, const json &, json_patch &, error_handler &) const override {} + void validate(const json::json_pointer &, json &, error_handler &) const override {} public: boolean_type(json &, root_schema *root) @@ -762,7 +760,7 @@ public: class boolean : public schema { bool true_; - void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override + void validate(const json::json_pointer &ptr, json &instance, error_handler &e) const override { if (!true_) { // false schema // empty array @@ -786,7 +784,7 @@ class required : public schema { const std::vector required_; - void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override final + void validate(const json::json_pointer &ptr, json &instance, error_handler &e) const override final { for (auto &r : required_) if (instance.find(r) == instance.end()) @@ -814,7 +812,7 @@ class object : public schema std::shared_ptr propertyNames_; - void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const override + void validate(const json::json_pointer &ptr, json &instance, error_handler &e) const override { if (maxProperties_.first && instance.size() > maxProperties_.second) e.error(ptr, instance, "too many properties"); @@ -828,15 +826,17 @@ class object : public schema // for each property in instance for (auto &p : instance.items()) { - if (propertyNames_) - propertyNames_->validate(ptr, p.key(), patch, e); + if (propertyNames_) { + json v = p.key(); + propertyNames_->validate(ptr, v, e); + } bool a_prop_or_pattern_matched = false; auto schema_p = properties_.find(p.key()); // check if it is in "properties" if (schema_p != properties_.end()) { a_prop_or_pattern_matched = true; - schema_p->second->validate(ptr / p.key(), p.value(), patch, e); + schema_p->second->validate(ptr / p.key(), p.value(), e); } #ifndef NO_STD_REGEX @@ -844,14 +844,14 @@ class object : public schema for (auto &schema_pp : patternProperties_) if (REGEX_NAMESPACE::regex_search(p.key(), schema_pp.first)) { a_prop_or_pattern_matched = true; - schema_pp.second->validate(ptr / p.key(), p.value(), patch, e); + schema_pp.second->validate(ptr / p.key(), p.value(), e); } #endif // check additionalProperties as a last resort if (!a_prop_or_pattern_matched && additionalProperties_) { first_error_handler additional_prop_err; - additionalProperties_->validate(ptr / p.key(), p.value(), patch, 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_); } @@ -863,15 +863,15 @@ class object : public schema if (instance.end() == finding) { // if the prop is not in the instance const auto &defaultValue = prop.second->defaultValue(ptr, instance, e); if (!defaultValue.empty()) { // if default value is available - patch.add((ptr / prop.first), defaultValue); + instance[prop.first] = defaultValue; } } } for (auto &dep : dependencies_) { auto prop = instance.find(dep.first); - if (prop != instance.end()) // if dependency-property is present in instance - dep.second->validate(ptr / dep.first, instance, patch, e); // validate + if (prop != instance.end()) // if dependency-property is present in instance + dep.second->validate(ptr / dep.first, instance, e); // validate } } @@ -966,7 +966,7 @@ class array : public schema std::shared_ptr contains_; - void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const override + void validate(const json::json_pointer &ptr, json &instance, error_handler &e) const override { if (maxItems_.first && instance.size() > maxItems_.second) e.error(ptr, instance, "array has too many items"); @@ -976,7 +976,7 @@ class array : public schema if (uniqueItems_) { for (auto it = instance.cbegin(); it != instance.cend(); ++it) { - auto v = std::find(it + 1, instance.end(), *it); + auto v = std::find(it + 1, instance.cend(), *it); if (v != instance.end()) e.error(ptr, instance, "items have to be unique for this array"); } @@ -985,7 +985,7 @@ class array : public schema size_t index = 0; if (items_schema_) for (auto &i : instance) { - items_schema_->validate(ptr / index, i, patch, e); + items_schema_->validate(ptr / index, i, e); index++; } else { @@ -1002,7 +1002,7 @@ class array : public schema if (!item_validator) break; - item_validator->validate(ptr / index, i, patch, e); + item_validator->validate(ptr / index, i, e); } } @@ -1010,7 +1010,7 @@ class array : public schema bool contained = false; for (auto &item : instance) { first_error_handler local_e; - contains_->validate(ptr, item, patch, local_e); + contains_->validate(ptr, item, local_e); if (!local_e) { contained = true; break; @@ -1227,9 +1227,21 @@ json json_validator::validate(const json &instance) const json json_validator::validate(const json &instance, error_handler &err) const { json::json_pointer ptr; - json_patch patch{}; - root_->validate(ptr, instance, patch, err); - return patch; + json cpy = instance; + root_->validate(ptr, cpy, err); + return json::diff(instance, cpy); +} + +void json_validator::validate_and_fill(json &instance) const +{ + throwing_error_handler err; + validate_and_fill(instance, err); +} + +void json_validator::validate_and_fill(json &instance, error_handler &err) const +{ + json::json_pointer ptr; + root_->validate(ptr, instance, err); } } // namespace json_schema diff --git a/src/nlohmann/json-schema.hpp b/src/nlohmann/json-schema.hpp index baa9719..6c1fbca 100644 --- a/src/nlohmann/json-schema.hpp +++ b/src/nlohmann/json-schema.hpp @@ -190,6 +190,12 @@ public: // validate a json-document based on the root-schema with a custom error-handler json validate(const json &, error_handler &) const; + + // validate a json-document based on the root-schema. It fills the document with default values where specified in the schema. + void validate_and_fill(json &) const; + + // validate a json-document based on the root-schema with a custom error-handler. It fills the document with default values where specified in the schema. + void validate_and_fill(json &, error_handler &) const; }; } // namespace json_schema diff --git a/test/issue-25-default-values.cpp b/test/issue-25-default-values.cpp index f775281..bada4a2 100644 --- a/test/issue-25-default-values.cpp +++ b/test/issue-25-default-values.cpp @@ -37,7 +37,7 @@ static const json person_schema = R"( "type": "object" })"_json; -int main(void) +int immutable_validation() { json_validator validator{}; @@ -95,6 +95,43 @@ int main(void) std::cerr << "Patch with defaults contains wrong value" << std::endl; return 1; } + return 0; +} + +int validation_and_fill() +{ + json_validator validator{}; + + // add address which is optional that should generate a diff containing a default street + json person_missing_address = R"({ + "name": "Knut", + "age": 12, + "address": {} +})"_json; + + validator.set_root_schema(person_schema); + + validator.validate_and_fill(person_missing_address); + + if (!person_missing_address.contains("/address/street"_json_pointer)) { + std::cerr << "Validated document should contain default value" << std::endl; + return 1; + } + + if (person_missing_address["/address/street"_json_pointer].get() != "Abbey Road") { + std::cerr << "Validated document with defaults contains wrong value" << std::endl; + return 1; + } return 0; } + +int main(void) +{ + int ret = 0; + + ret += immutable_validation(); + ret += validation_and_fill(); + + return ret; +}