Use json patch format for default

json patch (RFC 6902) makes it clearer in case of array contents. It is
supported by nlohmann::json and can be applied with `patch` to fill up
the json with defaults.
This commit is contained in:
Sven Fink 2020-03-03 11:04:23 +01:00
parent 75853c11ee
commit 6f3005dc75
5 changed files with 192 additions and 48 deletions

View File

@ -13,6 +13,7 @@ add_library(nlohmann_json_schema_validator
src/json-schema-draft7.json.cpp src/json-schema-draft7.json.cpp
src/json-uri.cpp src/json-uri.cpp
src/json-validator.cpp src/json-validator.cpp
src/json_patch.cpp
src/string-format-check.cpp) src/string-format-check.cpp)
target_include_directories(nlohmann_json_schema_validator target_include_directories(nlohmann_json_schema_validator

View File

@ -10,41 +10,65 @@ using nlohmann::json_schema::json_validator;
static json person_schema = R"( static json person_schema = R"(
{ {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "A person", "title": "Dict",
"properties": { "type": "array",
"name": { "items": {
"description": "Name", "title": "A person",
"type": "string" "properties": {
}, "name": {
"age": { "description": "Name",
"description": "Age of the person", "type": "string"
"type": "number", },
"minimum": 2, "age": {
"maximum": 200 "description": "Age of the person",
}, "type": "number",
"address":{ "minimum": 2,
"type": "object", "maximum": 200
"properties":{ },
"street":{ "address":{
"type": "string", "type": "object",
"default": "Abbey Road" "properties":{
"street":{
"type": "string",
"default": "Abbey Road"
}
} }
} }
} },
}, "required": [
"required": [ "name",
"name", "age"
"age" ],
], "type": "object"
"type": "object" }
} }
)"_json; )"_json;
// The people are defined with brace initialization // The people are defined with brace initialization
static json bad_person = {{"age", 42}}; static json bad_person = R"([
static json good_person = {{"name", "Albert"}, {"age", 42}, {"address", {{"street", "Main Street"}}}}; {"age": 42}
static json good_defaulted_person = {{"name", "Knut"}, {"age", 69}, {"address", {}}}; ])"_json;
static json good_person = R"([
{
"name": "Albert",
"age": 42,
"address": {"street": "Main Street"}
}
])"_json;
static json good_defaulted_person = R"([
{
"name": "Albert",
"age": 42,
"address": {"street": "Main Street"}
},
{
"name": "Knut",
"age": 69,
"address": {}
}
])"_json;
int main() int main()
{ {

View File

@ -8,11 +8,14 @@
*/ */
#include <nlohmann/json-schema.hpp> #include <nlohmann/json-schema.hpp>
#include "json_patch.hpp"
#include <memory> #include <memory>
#include <set> #include <set>
#include <sstream> #include <sstream>
using nlohmann::json; using nlohmann::json;
using nlohmann::json_patch;
using nlohmann::json_uri; using nlohmann::json_uri;
using nlohmann::json_schema::root_schema; using nlohmann::json_schema::root_schema;
using namespace nlohmann::json_schema; using namespace nlohmann::json_schema;
@ -43,7 +46,7 @@ public:
schema(root_schema *root) schema(root_schema *root)
: root_(root) {} : root_(root) {}
virtual void validate(const json::json_pointer &ptr, const json &instance, json &patch, error_handler &e) const = 0; virtual void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const = 0;
virtual const json &defaultValue(const json::json_pointer &, const json &, error_handler &) const virtual const json &defaultValue(const json::json_pointer &, const json &, error_handler &) const
{ {
@ -61,7 +64,7 @@ class schema_ref : public schema
const std::string id_; const std::string id_;
std::shared_ptr<schema> target_; std::shared_ptr<schema> target_;
void validate(const json::json_pointer &ptr, const json &instance, json &patch, error_handler &e) const final void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final
{ {
if (target_) if (target_)
target_->validate(ptr, instance, patch, e); target_->validate(ptr, instance, patch, e);
@ -227,7 +230,7 @@ public:
} while (1); } while (1);
} }
void validate(const json::json_pointer &ptr, const json &instance, json &patch, error_handler &e) const final void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final
{ {
if (root_) if (root_)
root_->validate(ptr, instance, patch, e); root_->validate(ptr, instance, patch, e);
@ -277,7 +280,7 @@ class logical_not : public schema
{ {
std::shared_ptr<schema> subschema_; std::shared_ptr<schema> subschema_;
void validate(const json::json_pointer &ptr, const json &instance, json &patch, error_handler &e) const final void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final
{ {
first_error_handler esub; first_error_handler esub;
subschema_->validate(ptr, instance, patch, esub); subschema_->validate(ptr, instance, patch, esub);
@ -312,7 +315,7 @@ class logical_combination : public schema
{ {
std::vector<std::shared_ptr<schema>> subschemata_; std::vector<std::shared_ptr<schema>> subschemata_;
void validate(const json::json_pointer &ptr, const json &instance, json &patch, error_handler &e) const final void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final
{ {
size_t count = 0; size_t count = 0;
@ -400,7 +403,7 @@ class type_schema : public schema
return defaultValue_; return defaultValue_;
} }
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, const json &instance, json_patch &patch, error_handler &e) const override final
{ {
// depending on the type of instance run the type specific validator - if present // depending on the type of instance run the type specific validator - if present
auto type = type_[(uint8_t) instance.type()]; auto type = type_[(uint8_t) instance.type()];
@ -587,7 +590,7 @@ class string : public schema
return len; return len;
} }
void validate(const json::json_pointer &ptr, const json &instance, json &, error_handler &e) const override void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override
{ {
if (minLength_.first) { if (minLength_.first) {
if (utf8_length(instance) < minLength_.second) { if (utf8_length(instance) < minLength_.second) {
@ -677,7 +680,7 @@ class numeric : public schema
return std::fabs(res) > std::fabs(eps); return std::fabs(res) > std::fabs(eps);
} }
void validate(const json::json_pointer &ptr, const json &instance, json &, error_handler &e) const override void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override
{ {
T value = instance; // conversion of json to value_type T value = instance; // conversion of json to value_type
@ -736,7 +739,7 @@ public:
class null : public schema class null : public schema
{ {
void validate(const json::json_pointer &ptr, const json &instance, json &, error_handler &e) const override void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override
{ {
if (!instance.is_null()) if (!instance.is_null())
e.error(ptr, instance, "expected to be null"); e.error(ptr, instance, "expected to be null");
@ -749,7 +752,7 @@ public:
class boolean_type : public schema class boolean_type : public schema
{ {
void validate(const json::json_pointer &, const json &, json &, error_handler &) const override {} void validate(const json::json_pointer &, const json &, json_patch &, error_handler &) const override {}
public: public:
boolean_type(json &, root_schema *root) boolean_type(json &, root_schema *root)
@ -759,7 +762,7 @@ public:
class boolean : public schema class boolean : public schema
{ {
bool true_; bool true_;
void validate(const json::json_pointer &ptr, const json &instance, json &, error_handler &e) const override void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override
{ {
if (!true_) { // false schema if (!true_) { // false schema
// empty array // empty array
@ -783,7 +786,7 @@ class required : public schema
{ {
const std::vector<std::string> required_; const std::vector<std::string> required_;
void validate(const json::json_pointer &ptr, const json &instance, json &, error_handler &e) const override final void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override final
{ {
for (auto &r : required_) for (auto &r : required_)
if (instance.find(r) == instance.end()) if (instance.find(r) == instance.end())
@ -811,7 +814,7 @@ class object : public schema
std::shared_ptr<schema> propertyNames_; std::shared_ptr<schema> propertyNames_;
void validate(const json::json_pointer &ptr, const json &instance, json &patch, error_handler &e) const override void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const override
{ {
if (maxProperties_.first && instance.size() > maxProperties_.second) if (maxProperties_.first && instance.size() > maxProperties_.second)
e.error(ptr, instance, "too many properties"); e.error(ptr, instance, "too many properties");
@ -860,7 +863,7 @@ class object : public schema
if (instance.end() == finding) { // if the prop is not in the instance if (instance.end() == finding) { // if the prop is not in the instance
const auto &defaultValue = prop.second->defaultValue(ptr, instance, e); const auto &defaultValue = prop.second->defaultValue(ptr, instance, e);
if (!defaultValue.empty()) { // if default value is available if (!defaultValue.empty()) { // if default value is available
patch[ptr / prop.first] = defaultValue; patch.add((ptr / prop.first), defaultValue);
} }
} }
} }
@ -963,7 +966,7 @@ class array : public schema
std::shared_ptr<schema> contains_; std::shared_ptr<schema> contains_;
void validate(const json::json_pointer &ptr, const json &instance, json &patch, error_handler &e) const override void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const override
{ {
if (maxItems_.first && instance.size() > maxItems_.second) if (maxItems_.first && instance.size() > maxItems_.second)
e.error(ptr, instance, "array has too many items"); e.error(ptr, instance, "array has too many items");
@ -1224,9 +1227,9 @@ json json_validator::validate(const json &instance) const
json json_validator::validate(const json &instance, error_handler &err) const json json_validator::validate(const json &instance, error_handler &err) const
{ {
json::json_pointer ptr; json::json_pointer ptr;
json mergePatch{}; json_patch patch{};
root_->validate(ptr, instance, mergePatch, err); root_->validate(ptr, instance, patch, err);
return mergePatch; return patch;
} }
} // namespace json_schema } // namespace json_schema

78
src/json_patch.cpp Normal file
View File

@ -0,0 +1,78 @@
#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<std::string>();
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<std::string>()};
} 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

38
src/json_patch.hpp Normal file
View File

@ -0,0 +1,38 @@
#pragma once
#include <nlohmann/json.hpp>
#include <string>
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