validator: rework everything to handle local and remote refs

Added json-uri and json-pointer-classes and a schema-preparation-pass.
This commit is contained in:
Patrick Boettcher 2016-12-25 18:50:28 +01:00
parent 43bd5b8e7d
commit b2240084fe
10 changed files with 1303 additions and 579 deletions

View File

@ -21,15 +21,18 @@ if(NOT TARGET json-hpp)
endif() endif()
# and one for the validator # and one for the validator
add_library(json-schema-validator INTERFACE) add_library(json-schema-validator
src/json-schema-draft4.json.cpp
src/json-uri.cpp
src/json-validator.cpp)
target_include_directories(json-schema-validator target_include_directories(json-schema-validator
INTERFACE PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/src) ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_compile_options(json-schema-validator target_compile_options(json-schema-validator
INTERFACE PUBLIC
-Wall -Wextra) # bad, better use something else based on compiler type -Wall -Wextra) # bad, better use something else based on compiler type
target_link_libraries(json-schema-validator target_link_libraries(json-schema-validator
INTERFACE PUBLIC
json-hpp) json-hpp)
install( install(
@ -43,10 +46,6 @@ install(
add_executable(json-schema-validate app/json-schema-validate.cpp) add_executable(json-schema-validate app/json-schema-validate.cpp)
target_link_libraries(json-schema-validate json-schema-validator) target_link_libraries(json-schema-validate json-schema-validator)
# json-schema-validator-tester
add_executable(json-schema-test app/json-schema-test.cpp)
target_link_libraries(json-schema-test json-schema-validator)
# test-zone # test-zone
enable_testing() enable_testing()
@ -56,6 +55,14 @@ find_path(JSON_SCHEMA_TEST_SUITE_PATH
tests/draft4) tests/draft4)
if(JSON_SCHEMA_TEST_SUITE_PATH) if(JSON_SCHEMA_TEST_SUITE_PATH)
# json-schema-validator-tester
add_executable(json-schema-test app/json-schema-test.cpp)
target_link_libraries(json-schema-test json-schema-validator)
target_compile_definitions(json-schema-test
PRIVATE
JSON_SCHEMA_TEST_SUITE_PATH="${JSON_SCHEMA_TEST_SUITE_PATH}")
# create tests foreach test-file # create tests foreach test-file
file(GLOB_RECURSE TEST_FILES ${JSON_SCHEMA_TEST_SUITE_PATH}/tests/draft4/*.json) file(GLOB_RECURSE TEST_FILES ${JSON_SCHEMA_TEST_SUITE_PATH}/tests/draft4/*.json)

View File

@ -84,7 +84,7 @@ int main(void)
There is an application which can be used for testing the validator with the There is an application which can be used for testing the validator with the
[JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite). [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite).
Currently **82** of ~**308** tests are still failing, because simply not all keywords and Currently **72** of ~**308** tests are still failing, because simply not all keywords and
their functionalities have been implemented. Some of the missing feature will their functionalities have been implemented. Some of the missing feature will
require a rework. Some will only work with external libraries. (remote references) require a rework. Some will only work with external libraries. (remote references)

View File

@ -1,11 +1,48 @@
#include "json-schema-validator.hpp" /*
* Modern C++ JSON schema validator
*
* Licensed under the MIT License <http://opensource.org/licenses/MIT>.
*
* Copyright (c) 2016 Patrick Boettcher <patrick.boettcher@posteo.de>.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom
* the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
* OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "json-schema.hpp"
#include <fstream>
using nlohmann::json; using nlohmann::json;
using nlohmann::json_validator; using nlohmann::json_uri;
using nlohmann::json_schema_draft4::json_validator;
int main(void) int main(void)
{ {
json validation; json validation; // a validation case following the JSON-test-suite-schema
std::map<std::string, std::string> external_schemas;
external_schemas["http://localhost:1234/integer.json"] =
JSON_SCHEMA_TEST_SUITE_PATH "/remotes/integer.json";
external_schemas["http://localhost:1234/subSchemas.json"] =
JSON_SCHEMA_TEST_SUITE_PATH "/remotes/subSchemas.json";
external_schemas["http://localhost:1234/folder/folderInteger.json"] =
JSON_SCHEMA_TEST_SUITE_PATH "/remotes/folder/folderInteger.json";
try { try {
std::cin >> validation; std::cin >> validation;
@ -25,22 +62,64 @@ int main(void)
const auto &schema = test_group["schema"]; const auto &schema = test_group["schema"];
json_validator validator;
do {
std::set<json_uri> undefined;
try {
undefined = validator.insert_schema(schema, json_uri("#"));
} catch (std::exception &e) {
std::cout << " Test Case Exception (root-schema-inserting): " << e.what() << "\n";
}
if (undefined.size() == 0)
break;
for (auto ref : undefined) {
std::cerr << "missing schema URL " << ref << " - trying to load it\n";
if (ref.to_string() == "http://json-schema.org/draft-04/schema#")
validator.insert_schema(nlohmann::json_schema_draft4::draft4_schema_builtin, ref);
else {
std::string fn = external_schemas[ref.url()];
std::fstream s(fn.c_str());
if (!s.good()) {
std::cerr << "could not open " << ref.url() << "\n";
return EXIT_FAILURE;
}
json extra;
extra << s;
try {
validator.insert_schema(extra, ref.url());
} catch (std::exception &e) {
std::cout << " Test Case Exception (schema-loading/inserting): " << e.what() << "\n";
}
}
}
} while (1);
for (auto &test_case : test_group["tests"]) { for (auto &test_case : test_group["tests"]) {
std::cout << " Testing Case " << test_case["description"] << "\n"; std::cout << " Testing Case " << test_case["description"] << "\n";
bool valid = true; bool valid = true;
try { try {
json_validator validator;
validator.set_schema("#", schema);
validator.validate(test_case["data"]); validator.validate(test_case["data"]);
} catch (const std::out_of_range &e) { } catch (const std::out_of_range &e) {
valid = false; valid = false;
std::cout << " Test Case Exception (out of range): " << e.what() << "\n"; std::cout << " Test Case Exception (out of range): " << e.what() << "\n";
} catch (const std::invalid_argument &e) { } catch (const std::invalid_argument &e) {
valid = false; valid = false;
std::cout << " Test Case Exception (invalid argument): " << e.what() << "\n"; std::cout << " Test Case Exception (invalid argument): " << e.what() << "\n";
} catch (const std::logic_error &e) { } catch (const std::logic_error &e) {
valid = !test_case["valid"]; /* force test-case failure */ valid = !test_case["valid"]; /* force test-case failure */
std::cout << " Not yet implemented: " << e.what() << "\n"; std::cout << " Not yet implemented: " << e.what() << "\n";
} }

View File

@ -1,31 +1,66 @@
#include "json-schema-validator.hpp" /*
* Modern C++ JSON schema validator
*
* Licensed under the MIT License <http://opensource.org/licenses/MIT>.
*
* Copyright (c) 2016 Patrick Boettcher <patrick.boettcher@posteo.de>.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom
* the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
* OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <json-schema.hpp>
#include <fstream> #include <fstream>
#include <set>
#include <cstdlib>
using nlohmann::json; using nlohmann::json;
using nlohmann::json_validator; using nlohmann::json_uri;
using nlohmann::json_schema_draft4::json_validator;
static void usage(const char *name) static void usage(const char *name)
{ {
std::cerr << "Usage: " << name << " <json-document> < <schema>\n"; std::cerr << "Usage: " << name << " <schema> < <document>\n";
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
#if 0
resolver r(nlohmann::json_schema_draft4::root_schema,
nlohmann::json_schema_draft4::root_schema["id"]);
schema_refs_.insert(r.schema_refs.begin(), r.schema_refs.end());
assert(r.undefined_refs.size() == 0);
#endif
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
if (argc != 2) if (argc != 2)
usage(argv[0]); usage(argv[0]);
json_validator validator;
std::fstream f(argv[1]); std::fstream f(argv[1]);
if (!f.good()) { if (!f.good()) {
std::cerr << "could not open " << argv[1] << " for reading\n"; std::cerr << "could not open " << argv[1] << " for reading\n";
usage(argv[0]); usage(argv[0]);
} }
// 1) Read the schema for the document you want to validate
json schema; json schema;
try { try {
f >> schema; f >> schema;
} catch (std::exception &e) { } catch (std::exception &e) {
@ -33,19 +68,46 @@ int main(int argc, char *argv[])
return EXIT_FAILURE; return EXIT_FAILURE;
} }
json document; // 2) insert this schema to the validator
// this resolves remote-schemas, sub-schemas and references
bool error = false;
do {
// inserting with json_uri("#") means this is the document's root-schema
auto missing_schemas = validator.insert_schema(schema, json_uri("#"));
// schema was inserted and all references have been fulfilled
if (missing_schemas.size() == 0)
break;
// schema was not inserted because it references unknown schemas
// 3) load missing schemas and insert them
for (auto ref : missing_schemas) {
std::cerr << "missing schema URL " << ref << " - trying to load it\n";
std::fstream lf(ref.path());
if (!lf.good()) {
std::cerr << "could not open " << ref.url() << "\n";
error = true;
break;
}
json extra;
try { try {
std::cin >> document; lf >> extra;
} catch (std::exception &e) { } catch (std::exception &e) {
std::cerr << e.what() << " at " << f.tellp() << "\n"; std::cerr << e.what() << " at " << lf.tellp() << "\n";
return EXIT_FAILURE; return EXIT_FAILURE;
} }
validator.insert_schema(extra, json_uri(ref.url()));
std::cerr << "OK";
}
} while (!error);
// 4) do the actual validation of the document
json document;
try { try {
json_validator validator; document << std::cin;
validator.set_schema("#", schema);
validator.validate(document); validator.validate(document);
} catch (std::exception &e) { } catch (std::exception &e) {
std::cerr << "schema validation failed\n"; std::cerr << "schema validation failed\n";
@ -53,7 +115,7 @@ int main(int argc, char *argv[])
return EXIT_FAILURE; return EXIT_FAILURE;
} }
std::cerr << std::setw(2) << document << "\n"; std::cerr << "document is valid\n";
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }

View File

@ -0,0 +1,157 @@
#include <json-schema.hpp>
namespace nlohmann::json_schema_draft4
{
json draft4_schema_builtin = R"( {
"id": "http://json-schema.org/draft-04/schema#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Core schema meta-schema",
"definitions": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#" }
},
"positiveInteger": {
"type": "integer",
"minimum": 0
},
"positiveIntegerDefault0": {
"allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]
},
"simpleTypes": {
"enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"uniqueItems": true
}
},
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uri"
},
"$schema": {
"type": "string",
"format": "uri"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": {},
"multipleOf": {
"type": "number",
"minimum": 0,
"exclusiveMinimum": true
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "boolean",
"default": false
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "boolean",
"default": false
},
"maxLength": { "$ref": "#/definitions/positiveInteger" },
"minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"additionalItems": {
"anyOf": [
{ "type": "boolean" },
{ "$ref": "#" }
],
"default": {}
},
"items": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/schemaArray" }
],
"default": {}
},
"maxItems": { "$ref": "#/definitions/positiveInteger" },
"minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"maxProperties": { "$ref": "#/definitions/positiveInteger" },
"minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
"required": { "$ref": "#/definitions/stringArray" },
"additionalProperties": {
"anyOf": [
{ "type": "boolean" },
{ "$ref": "#" }
],
"default": {}
},
"definitions": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"properties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"dependencies": {
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/stringArray" }
]
}
},
"enum": {
"type": "array",
"minItems": 1,
"uniqueItems": true
},
"type": {
"anyOf": [
{ "$ref": "#/definitions/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/definitions/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"allOf": { "$ref": "#/definitions/schemaArray" },
"anyOf": { "$ref": "#/definitions/schemaArray" },
"oneOf": { "$ref": "#/definitions/schemaArray" },
"not": { "$ref": "#" }
},
"dependencies": {
"exclusiveMaximum": [ "maximum" ],
"exclusiveMinimum": [ "minimum" ]
},
"default": {}
} )"_json;
}

View File

@ -1,548 +0,0 @@
/*
* Modern C++ JSON schema validator
*
* Licensed under the MIT License <http://opensource.org/licenses/MIT>.
*
* Copyright (c) 2016 Patrick Boettcher <patrick.boettcher@posteo.de>.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom
* the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
* OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef NLOHMANN_JSON_VALIDATOR_HPP__
#define NLOHMANN_JSON_VALIDATOR_HPP__
#include <json.hpp>
#include <regex>
#include <set>
// make yourself a home - welcome to nlohmann's namespace
namespace nlohmann
{
class json_validator
{
// insert default values items into object
// if the key is not present before checking their
// validity in regards to their schema
//
// breaks JSON-Schema-Test-Suite if true
// *PARTIALLY IMPLEMENTED* only for properties of objects
bool default_value_insertion = false;
// recursively insert default values and create parent objects if
// they would be empty
//
// breaks JSON-Schema-Test-Suite if true
// *NOT YET IMPLEMENTED* -> maybe the same as the above option, need more thoughts
bool recursive_default_value_insertion = false;
void not_yet_implemented(const json &schema, const std::string &field, const std::string &type)
{
if (schema.find(field) != schema.end())
throw std::logic_error(field + " for " + type + " is not yet implemented");
}
void validate_type(const json &schema, const std::string &expected_type, const std::string &name)
{
const auto &type_it = schema.find("type");
if (type_it == schema.end())
/* TODO guess type for more safety,
* TODO use definitions
* TODO valid by not being defined? FIXME not clear - there are
* schema-test case which are not specifying a type */
return;
const auto &type_instance = type_it.value();
// any of the types in this array
if (type_instance.type() == json::value_t::array) {
if (std::find(type_instance.begin(),
type_instance.end(),
expected_type) != type_instance.end())
return;
std::ostringstream s;
s << expected_type << " is not any of " << type_instance << " for " << name;
throw std::invalid_argument(s.str());
} else { // type_instance is a string
if (type_instance == expected_type)
return;
throw std::invalid_argument(type_instance.get<std::string>() + " is not a " + expected_type + " for " + name);
}
}
void validate_enum(json &instance, const json &schema, const std::string &name)
{
const auto &enum_value = schema.find("enum");
if (enum_value == schema.end())
return;
if (std::find(enum_value.value().begin(), enum_value.value().end(), instance) != enum_value.value().end())
return;
std::ostringstream s;
s << "invalid enum-value '" << instance << "' "
<< "for instance '" << name << "'. Candidates are " << enum_value.value() << ".";
throw std::invalid_argument(s.str());
}
void validate_string(json &instance, const json &schema, const std::string &name)
{
// possibile but unhanled keywords
not_yet_implemented(schema, "format", "string");
not_yet_implemented(schema, "pattern", "string");
validate_type(schema, "string", name);
// minLength
auto attr = schema.find("minLength");
if (attr != schema.end())
if (instance.get<std::string>().size() < attr.value()) {
std::ostringstream s;
s << "'" << name << "' of value '" << instance << "' is too short as per minLength ("
<< attr.value() << ")";
throw std::out_of_range(s.str());
}
// maxLength
attr = schema.find("maxLength");
if (attr != schema.end())
if (instance.get<std::string>().size() > attr.value()) {
std::ostringstream s;
s << "'" << name << "' of value '" << instance << "' is too long as per maxLength ("
<< attr.value() << ")";
throw std::out_of_range(s.str());
}
}
void validate_boolean(json & /*instance*/, const json &schema, const std::string &name)
{
validate_type(schema, "boolean", name);
}
void validate_numeric(json &instance, const json &schema, const std::string &name)
{
double value = instance;
const auto &multipleOf = schema.find("multipleOf");
if (multipleOf != schema.end()) {
double rem = fmod(value, multipleOf.value());
if (rem != 0.0)
throw std::out_of_range(name + " is not a multiple ...");
}
const auto &maximum = schema.find("maximum");
if (maximum != schema.end()) {
double maxi = maximum.value();
auto ex = std::out_of_range(name + " exceeds maximum of ...");
if (schema.find("exclusiveMaximum") != schema.end()) {
if (value >= maxi)
throw ex;
} else {
if (value > maxi)
throw ex;
}
}
const auto &minimum = schema.find("minimum");
if (minimum != schema.end()) {
double mini = minimum.value();
auto ex = std::out_of_range(name + " exceeds minimum of ...");
if (schema.find("exclusiveMinimum") != schema.end()) {
if (value <= mini)
throw ex;
} else {
if (value < mini)
throw ex;
}
}
}
void validate_integer(json &instance, const json &schema, const std::string &name)
{
validate_type(schema, "integer", name);
validate_numeric(instance, schema, name);
}
void validate_unsigned(json &instance, const json &schema, const std::string &name)
{
validate_type(schema, "integer", name);
validate_numeric(instance, schema, name);
}
void validate_float(json &instance, const json &schema, const std::string &name)
{
validate_type(schema, "number", name);
validate_numeric(instance, schema, name);
}
void validate_null(json & /*instance*/, const json &schema, const std::string &name)
{
validate_type(schema, "null", name);
}
void validate_array(json &instance, const json &schema, const std::string &name)
{
validate_type(schema, "array", name);
// maxItems
const auto &maxItems = schema.find("maxItems");
if (maxItems != schema.end())
if (instance.size() > maxItems.value())
throw std::out_of_range(name + " has too many items.");
// minItems
const auto &minItems = schema.find("minItems");
if (minItems != schema.end())
if (instance.size() < minItems.value())
throw std::out_of_range(name + " has too many items.");
// uniqueItems
const auto &uniqueItems = schema.find("uniqueItems");
if (uniqueItems != schema.end())
if (uniqueItems.value() == true) {
std::set<json> array_to_set;
for (auto v : instance) {
auto ret = array_to_set.insert(v);
if (ret.second == false)
throw std::out_of_range(name + " should have only unique items.");
}
}
// items and additionalItems
// default to empty schemas
auto items_iter = schema.find("items");
json items = {};
if (items_iter != schema.end())
items = items_iter.value();
auto additionalItems_iter = schema.find("additionalItems");
json additionalItems = {};
if (additionalItems_iter != schema.end())
additionalItems = additionalItems_iter.value();
size_t i = 0;
bool validation_done = false;
for (auto &value : instance) {
std::string sub_name = name + "[" + std::to_string(i) + "]";
switch (items.type()) {
case json::value_t::array:
if (i < items.size())
validate(value, items[i], sub_name);
else {
switch (additionalItems.type()) { // items is an array
// we need to take into consideration additionalItems
case json::value_t::object:
validate(value, additionalItems, sub_name);
break;
case json::value_t::boolean:
if (additionalItems == false)
throw std::out_of_range("additional values in array are not allowed for " + sub_name);
else
validation_done = true;
break;
default:
break;
}
}
break;
case json::value_t::object: // items is a schema
validate(value, items, sub_name);
break;
default:
break;
}
if (validation_done)
break;
i++;
}
}
void validate_object(json &instance, const json &schema, const std::string &name)
{
validate_type(schema, "object", name);
json properties = {};
if (schema.find("properties") != schema.end())
properties = schema["properties"];
// check for default values of properties
// and insert them into this object, if they don't exists
// works only for object properties for the moment
if (default_value_insertion)
for (auto it = properties.begin(); it != properties.end(); ++it) {
const auto &default_value = it.value().find("default");
if (default_value == it.value().end())
continue; /* no default value -> continue */
if (instance.find(it.key()) != instance.end())
continue; /* value is present */
/* create element from default value */
instance[it.key()] = default_value.value();
}
// maxProperties
const auto &maxProperties = schema.find("maxProperties");
if (maxProperties != schema.end())
if (instance.size() > maxProperties.value())
throw std::out_of_range(name + " has too many properties.");
// minProperties
const auto &minProperties = schema.find("minProperties");
if (minProperties != schema.end())
if (instance.size() < minProperties.value())
throw std::out_of_range(name + " has too few properties.");
// additionalProperties
enum {
True,
False,
Object
} additionalProperties = True;
const auto &additionalPropertiesVal = schema.find("additionalProperties");
if (additionalPropertiesVal != schema.end()) {
if (additionalPropertiesVal.value().type() == json::value_t::boolean)
additionalProperties = additionalPropertiesVal.value() == true ? True : False;
else
additionalProperties = Object;
}
// patternProperties
json patternProperties = {};
if (schema.find("patternProperties") != schema.end())
patternProperties = schema["patternProperties"];
// check all elements in object
for (auto child = instance.begin(); child != instance.end(); ++child) {
std::string child_name = name + "." + child.key();
// is this a property which is described in the schema
const auto &object_prop = properties.find(child.key());
if (object_prop != properties.end()) {
// validate the element with its schema
validate(child.value(), object_prop.value(), child_name);
continue;
}
bool patternProperties_has_matched = false;
for (auto pp = patternProperties.begin();
pp != patternProperties.end(); ++pp) {
std::regex re(pp.key(), std::regex::ECMAScript);
if (std::regex_search(child.key(), re)) {
validate(child.value(), pp.value(), child_name);
patternProperties_has_matched = true;
}
}
if (patternProperties_has_matched)
continue;
switch (additionalProperties) {
case True:
break;
case Object:
validate(child.value(), additionalPropertiesVal.value(), child_name);
break;
case False:
throw std::invalid_argument("unknown property '" + child.key() + "' in object '" + name + "'");
break;
};
}
// required
const auto &required = schema.find("required");
if (required != schema.end())
for (const auto &element : required.value()) {
if (instance.find(element) == instance.end()) {
throw std::invalid_argument("required element '" + element.get<std::string>() +
"' not found in object '" + name + "'");
}
}
// dependencies
const auto &dependencies = schema.find("dependencies");
if (dependencies == schema.end())
return;
for (auto dep = dependencies.value().cbegin();
dep != dependencies.value().cend();
++dep) {
// property not present in this instance - next
if (instance.find(dep.key()) == instance.end())
continue;
std::string sub_name = name + ".dependency-of-" + dep.key();
switch (dep.value().type()) {
case json::value_t::object:
validate(instance, dep.value(), sub_name);
break;
case json::value_t::array:
for (const auto &prop : dep.value())
if (instance.find(prop) == instance.end())
throw std::invalid_argument("failed dependency for " + sub_name + ". Need property " + prop.get<std::string>());
break;
default:
break;
}
}
}
void validate(json &instance, const json &schema_, const std::string &name)
{
not_yet_implemented(schema_, "allOf", "all");
not_yet_implemented(schema_, "anyOf", "all");
not_yet_implemented(schema_, "oneOf", "all");
not_yet_implemented(schema_, "not", "all");
// std::cerr << instance << " VS\n";
// std::cerr << schema_ << "\n";
// std::cerr << "\n";
const json *schema = &schema_;
do {
const auto &ref = schema->find("$ref");
if (ref != schema->end()) {
std::string r = ref.value();
// do we have stored a schema which correspond to this reference
if (schema_references.find(r) == schema_references.end()) { // no
if (r[0] != '#')
throw std::logic_error("remote references are not yet implemented for ref " + r);
schema = schema_references["#"]; // root schema
// Aieee, need so much better parsing than this TODO
r = r.substr(1); // skip '#'
std::regex re("\\/([a-zA-Z0-9~%]+)");
for (auto match = std::sregex_iterator(r.begin(), r.end(), re), end = std::sregex_iterator();
match != end;
++match) {
std::string name = match->str().substr(1);
switch (schema->type()) {
case json::value_t::array: {
auto index = std::stoul(name);
if (index >= schema->size())
throw std::out_of_range("reference schema " + r + " is out of range for array-reference\n");
schema = &((*schema)[index]);
} break;
case json::value_t::object: {
const auto &sub = schema->find(name);
if (sub == schema->end())
throw std::invalid_argument("reference schema " + r + " not found for object\n");
schema = &(*sub);
} break;
default:
break;
}
}
schema_references[r] = schema;
} else
schema = schema_references[r];
} else
break;
} while (1); // loop in case of nested refs
validate_enum(instance, *schema, name);
switch (instance.type()) {
case json::value_t::object:
validate_object(instance, *schema, name);
break;
case json::value_t::array:
validate_array(instance, *schema, name);
break;
case json::value_t::string:
validate_string(instance, *schema, name);
break;
case json::value_t::number_unsigned:
validate_unsigned(instance, *schema, name);
break;
case json::value_t::number_integer:
validate_integer(instance, *schema, name);
break;
case json::value_t::number_float:
validate_float(instance, *schema, name);
break;
case json::value_t::boolean:
validate_boolean(instance, *schema, name);
break;
case json::value_t::null:
validate_null(instance, *schema, name);
break;
default:
assert(0 && "unexpected instance type for validation");
break;
}
}
public:
std::map<std::string, const json *> schema_references;
void set_schema(const std::string &ref, const json &schema)
{
schema_references[ref] = &schema; /* replace or insert */
}
void validate(json &instance)
{
validate(instance, *schema_references["#"], "root");
}
};
}
#endif /* NLOHMANN_JSON_VALIDATOR_HPP__ */

201
src/json-schema.hpp Normal file
View File

@ -0,0 +1,201 @@
/*
* Modern C++ JSON schema validator
*
* Licensed under the MIT License <http://opensource.org/licenses/MIT>.
*
* Copyright (c) 2016 Patrick Boettcher <patrick.boettcher@posteo.de>.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom
* the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
* OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef NLOHMANN_JSON_SCHEMA_HPP__
#define NLOHMANN_JSON_SCHEMA_HPP__
#include <json.hpp>
#include <set>
// make yourself a home - welcome to nlohmann's namespace
namespace nlohmann
{
// a class representing a JSON-pointer RFC6901
//
// examples of JSON pointers
//
// # - root of the current document
// #item - refers to the object which is identified ("id") by `item`
// in the current document
// #/path/to/element
// - refers to the element in /path/to from the root-document
//
//
// The json_pointer-class stores everything in a string, which might seem bizarre
// as parsing is done from a string to a string, but from_string() is also
// doing some formatting.
//
// TODO
// ~ and % - codec
// needs testing and clarification regarding the '#' at the beginning
class json_pointer
{
std::string str_;
void from_string(const std::string &r);
public:
json_pointer(const std::string &s = "")
{
from_string(s);
}
void append(const std::string &elem)
{
str_.append(elem);
}
const std::string &to_string() const
{
return str_;
}
};
// A class representing a JSON-URI for schemas derived from
// section 8 of JSON Schema: A Media Type for Describing JSON Documents
// draft-wright-json-schema-00
//
// New URIs can be derived from it using the derive()-method.
// This is useful for resolving refs or subschema-IDs in json-schemas.
//
// This is done implement the requirements described in section 8.2.
//
class json_uri
{
std::string urn_;
std::string proto_;
std::string hostname_;
std::string path_;
json_pointer pointer_;
protected:
// decodes a JSON uri and replaces all or part of the currently stored values
void from_string(const std::string &uri);
std::tuple<std::string, std::string, std::string, std::string, std::string> tie() const
{
return std::tie(urn_, proto_, hostname_, path_, pointer_.to_string());
}
public:
json_uri(const std::string &uri)
{
from_string(uri);
}
const std::string protocol() const { return proto_; }
const std::string hostname() const { return hostname_; }
const std::string path() const { return path_; }
const json_pointer pointer() const { return pointer_; }
const std::string url() const;
json_uri derive(const std::string &uri) const
{
json_uri u = *this;
u.from_string(uri);
return u;
}
json_uri append(const std::string &field) const
{
json_uri u = *this;
u.pointer_.append("/" + field);
return u;
}
std::string to_string() const;
friend bool operator<(const json_uri &l, const json_uri &r)
{
return l.tie() < r.tie();
}
friend bool operator==(const json_uri &l, const json_uri &r)
{
return l.tie() == r.tie();
}
friend std::ostream &operator<<(std::ostream &os, const json_uri &u);
};
//
namespace json_schema_draft4
{
extern json draft4_schema_builtin;
class json_validator
{
std::vector<std::shared_ptr<json>> schema_store_;
std::shared_ptr<json> root_schema_;
std::map<json_uri, const json *> schema_refs_;
void not_yet_implemented(const json &schema, const std::string &field, const std::string &type);
void validate_type(const json &schema, const std::string &expected_type, const std::string &name);
void validate_enum(json &instance, const json &schema, const std::string &name);
void validate_numeric(json &instance, const json &schema, const std::string &name);
void validate(json &instance, const json &schema, const std::string &name);
void validate_string(json &instance, const json &schema, const std::string &name);
void validate_boolean(json &instance, const json &schema, const std::string &name);
void validate_integer(json &instance, const json &schema, const std::string &name);
void validate_unsigned(json &instance, const json &schema, const std::string &name);
void validate_float(json &instance, const json &schema, const std::string &name);
void validate_null(json &instance, const json &schema, const std::string &name);
void validate_array(json &instance, const json &schema, const std::string &name);
void validate_object(json &instance, const json &schema, const std::string &name);
public:
std::set<json_uri> insert_schema(const json &input, json_uri id);
void validate(json &instance);
// insert default values items into object
// if the key is not present before checking their
// validity in regards to their schema
//
// breaks JSON-Schema-Test-Suite if true
// *PARTIALLY IMPLEMENTED* only for properties of objects
bool default_value_insertion = false;
// recursively insert default values and create parent objects if
// they would be empty
//
// breaks JSON-Schema-Test-Suite if true
// *NOT YET IMPLEMENTED* -> maybe the same as the above option, need more thoughts
bool recursive_default_value_insertion = false;
};
} // json_schema_draft4
} // nlohmann
#endif /* NLOHMANN_JSON_SCHEMA_HPP__ */

127
src/json-uri.cpp Normal file
View File

@ -0,0 +1,127 @@
/*
* Modern C++ JSON schema validator
*
* Licensed under the MIT License <http://opensource.org/licenses/MIT>.
*
* Copyright (c) 2016 Patrick Boettcher <patrick.boettcher@posteo.de>.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom
* the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
* OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "json-schema.hpp"
namespace nlohmann {
void json_pointer::from_string(const std::string &r)
{
str_ = "#";
if (r.size() == 0)
return;
if (r[0] != '#')
throw std::invalid_argument("not a valid JSON pointer - missing # at the beginning");
if (r.size() == 1)
return;
std::size_t pos = 1;
do {
std::size_t next = r.find('/', pos + 1);
str_.append(r.substr(pos, next - pos));
pos = next;
} while (pos != std::string::npos);
}
void json_uri::from_string(const std::string &uri)
{
// if it is an urn take it as it is - maybe there is more to be done
if (uri.find("urn:") == 0) {
urn_ = uri;
return;
}
std::string pointer = "#"; // default pointer is the root
// first split the URI into URL and JSON-pointer
auto pointer_separator = uri.find('#');
if (pointer_separator != std::string::npos) // and extract the JSON-pointer-string if found
pointer = uri.substr(pointer_separator);
// the rest is an URL
std::string url = uri.substr(0, pointer_separator);
if (url.size()) { // if an URL is part of the URI
std::size_t pos = 0;
auto proto = url.find("://", pos);
if (proto != std::string::npos) { // extract the protocol
proto_ = url.substr(pos, proto - pos);
pos = 3 + proto; // 3 == "://"
auto hostname = url.find("/", pos);
if (hostname != std::string::npos) { // and the hostname (no proto without hostname)
hostname_ = url.substr(pos, hostname - pos);
pos = hostname;
}
}
// the rest is the path
auto path = url.substr(pos);
if (path[0] == '/') // if it starts with a / it is root-path
path_ = path;
else // otherwise it is a subfolder
path_.append(path);
pointer_ = json_pointer("");
}
if (pointer.size() > 0)
pointer_ = pointer;
}
const std::string json_uri::url() const
{
std::stringstream s;
if (proto_.size() > 0)
s << proto_ << "://";
s << hostname_
<< path_;
return s.str();
}
std::string json_uri::to_string() const
{
std::stringstream s;
s << urn_
<< url()
<< pointer_.to_string();
return s.str();
}
std::ostream &operator<<(std::ostream &os, const json_uri &u)
{
return os << u.to_string();
}
}

610
src/json-validator.cpp Normal file
View File

@ -0,0 +1,610 @@
/*
* Modern C++ JSON schema validator
*
* Licensed under the MIT License <http://opensource.org/licenses/MIT>.
*
* Copyright (c) 2016 Patrick Boettcher <patrick.boettcher@posteo.de>.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom
* the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
* OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <json-schema.hpp>
#include <regex>
#include <set>
using nlohmann::json;
using nlohmann::json_uri;
namespace {
class resolver
{
void resolve(json &schema, json_uri id)
{
auto fid = schema.find("id");
if (fid != schema.end() &&
fid.value().type() == json::value_t::string)
id = id.derive(fid.value());
if (schema_refs.find(id) != schema_refs.end())
throw std::invalid_argument("schema " + id.to_string() + " already present in local resolver");
// store a raw pointer to this (sub-)schema references by its absolute json_uri
// this (sub-)schema is part of a schema stored inside schema_store_
schema_refs[id] = &schema;
for (auto i = schema.begin(), end = schema.end(); i != end; ++i) {
if (i.key() == "default") /* default value can be objects, but are not schemas */
continue;
switch (i.value().type()) {
case json::value_t::object: // child is object, it is a schema
resolve(i.value(), id.append(i.key()));
break;
case json::value_t::array: {
std::size_t index = 0;
auto child_id = id.append(i.key());
for (auto &v : i.value()) {
if (v.type() == json::value_t::object) // array element is object
resolve(v, child_id.append(std::to_string(index)) );
index++;
}
} break;
case json::value_t::string:
if (i.key() == "$ref") {
json_uri ref = id.derive(i.value());
i.value() = ref.to_string();
refs.insert(ref);
}
break;
default:
break;
}
}
}
std::set<json_uri> refs;
public:
std::set<json_uri> undefined_refs;
std::map<json_uri, const json *> schema_refs;
resolver(json &schema, json_uri id)
{
// if schema has an id use it as name and to retrieve the namespace (URL)
auto fid = schema.find("id");
if (fid != schema.end())
id = id.derive(fid.value());
resolve(schema, id);
// refs now contains all references
//
// local references should be resolvable inside the same URL
//
// undefined_refs will only contain external references
for (auto r : refs) {
if (schema_refs.find(r) == schema_refs.end()) {
if (r.url() == id.url()) // same url means referencing a sub-schema
// of the same document, which has not been found
throw std::invalid_argument("sub-schema " + r.pointer().to_string() +
" in schema " + id.to_string() + " not found");
undefined_refs.insert(r);
}
}
}
};
} // anonymous namespace
namespace nlohmann {
namespace json_schema_draft4
{
std::set<json_uri> json_validator::insert_schema(const json &input, json_uri id)
{
// allocate create a copy for later storage - if resolving reference works
std::shared_ptr<json> schema = std::make_shared<json>(input);
// resolve all local schemas and references
resolver r(*schema, id);
// check whether all undefined schema references can be resolved with existing ones
std::set<json_uri> undefined;
for (auto &ref : r.undefined_refs)
if (schema_refs_.find(ref) == schema_refs_.end()) { // exact schema reference not found
undefined.insert(ref);
}
// anything cannot be resolved, inform the user and make him/her load additional schemas
// before retrying
if (undefined.size() > 0)
return undefined;
// check whether all schema-references are new
for (auto &sref : r.schema_refs) {
if (schema_refs_.find(sref.first) != schema_refs_.end())
throw std::invalid_argument("schema " + sref.first.to_string() + " already present in validator.");
}
// no undefined references and no duplicated schema - store the schema
schema_store_.push_back(schema);
// and insert all references
schema_refs_.insert(r.schema_refs.begin(), r.schema_refs.end());
// store the document root-schema
if (id == json_uri("#"))
root_schema_ = schema;
return undefined;
}
void json_validator::not_yet_implemented(const json &schema, const std::string &field, const std::string &type)
{
if (schema.find(field) != schema.end())
throw std::logic_error(field + " for " + type + " is not yet implemented");
}
void json_validator::validate_type(const json &schema, const std::string &expected_type, const std::string &name)
{
const auto &type_it = schema.find("type");
if (type_it == schema.end())
/* TODO guess type for more safety,
* TODO use definitions
* TODO valid by not being defined? FIXME not clear - there are
* schema-test case which are not specifying a type */
return;
const auto &type_instance = type_it.value();
// any of the types in this array
if (type_instance.type() == json::value_t::array) {
if (std::find(type_instance.begin(),
type_instance.end(),
expected_type) != type_instance.end())
return;
std::ostringstream s;
s << expected_type << " is not any of " << type_instance << " for " << name;
throw std::invalid_argument(s.str());
} else { // type_instance is a string
if (type_instance == expected_type)
return;
throw std::invalid_argument(type_instance.get<std::string>() + " is not a " + expected_type + " for " + name);
}
}
void json_validator::validate_enum(json &instance, const json &schema, const std::string &name)
{
const auto &enum_value = schema.find("enum");
if (enum_value == schema.end())
return;
if (std::find(enum_value.value().begin(), enum_value.value().end(), instance) != enum_value.value().end())
return;
std::ostringstream s;
s << "invalid enum-value '" << instance << "' "
<< "for instance '" << name << "'. Candidates are " << enum_value.value() << ".";
throw std::invalid_argument(s.str());
}
void json_validator::validate_string(json &instance, const json &schema, const std::string &name)
{
// possibile but unhanled keywords
not_yet_implemented(schema, "format", "string");
not_yet_implemented(schema, "pattern", "string");
validate_type(schema, "string", name);
// minLength
auto attr = schema.find("minLength");
if (attr != schema.end())
if (instance.get<std::string>().size() < attr.value()) {
std::ostringstream s;
s << "'" << name << "' of value '" << instance << "' is too short as per minLength ("
<< attr.value() << ")";
throw std::out_of_range(s.str());
}
// maxLength
attr = schema.find("maxLength");
if (attr != schema.end())
if (instance.get<std::string>().size() > attr.value()) {
std::ostringstream s;
s << "'" << name << "' of value '" << instance << "' is too long as per maxLength ("
<< attr.value() << ")";
throw std::out_of_range(s.str());
}
}
void json_validator::validate_boolean(json & /*instance*/, const json &schema, const std::string &name)
{
validate_type(schema, "boolean", name);
}
void json_validator::validate_numeric(json &instance, const json &schema, const std::string &name)
{
double value = instance;
const auto &multipleOf = schema.find("multipleOf");
if (multipleOf != schema.end()) {
double rem = fmod(value, multipleOf.value());
if (rem != 0.0)
throw std::out_of_range(name + " is not a multiple ...");
}
const auto &maximum = schema.find("maximum");
if (maximum != schema.end()) {
double maxi = maximum.value();
auto ex = std::out_of_range(name + " exceeds maximum of ...");
if (schema.find("exclusiveMaximum") != schema.end()) {
if (value >= maxi)
throw ex;
} else {
if (value > maxi)
throw ex;
}
}
const auto &minimum = schema.find("minimum");
if (minimum != schema.end()) {
double mini = minimum.value();
auto ex = std::out_of_range(name + " exceeds minimum of ...");
if (schema.find("exclusiveMinimum") != schema.end()) {
if (value <= mini)
throw ex;
} else {
if (value < mini)
throw ex;
}
}
}
void json_validator::validate_integer(json &instance, const json &schema, const std::string &name)
{
validate_type(schema, "integer", name);
validate_numeric(instance, schema, name);
}
void json_validator::validate_unsigned(json &instance, const json &schema, const std::string &name)
{
validate_type(schema, "integer", name);
validate_numeric(instance, schema, name);
}
void json_validator::validate_float(json &instance, const json &schema, const std::string &name)
{
validate_type(schema, "number", name);
validate_numeric(instance, schema, name);
}
void json_validator::validate_null(json & /*instance*/, const json &schema, const std::string &name)
{
validate_type(schema, "null", name);
}
void json_validator::validate_array(json &instance, const json &schema, const std::string &name)
{
validate_type(schema, "array", name);
// maxItems
const auto &maxItems = schema.find("maxItems");
if (maxItems != schema.end())
if (instance.size() > maxItems.value())
throw std::out_of_range(name + " has too many items.");
// minItems
const auto &minItems = schema.find("minItems");
if (minItems != schema.end())
if (instance.size() < minItems.value())
throw std::out_of_range(name + " has too many items.");
// uniqueItems
const auto &uniqueItems = schema.find("uniqueItems");
if (uniqueItems != schema.end())
if (uniqueItems.value() == true) {
std::set<json> array_to_set;
for (auto v : instance) {
auto ret = array_to_set.insert(v);
if (ret.second == false)
throw std::out_of_range(name + " should have only unique items.");
}
}
// items and additionalItems
// default to empty schemas
auto items_iter = schema.find("items");
json items = {};
if (items_iter != schema.end())
items = items_iter.value();
auto additionalItems_iter = schema.find("additionalItems");
json additionalItems = {};
if (additionalItems_iter != schema.end())
additionalItems = additionalItems_iter.value();
size_t i = 0;
bool validation_done = false;
for (auto &value : instance) {
std::string sub_name = name + "[" + std::to_string(i) + "]";
switch (items.type()) {
case json::value_t::array:
if (i < items.size())
validate(value, items[i], sub_name);
else {
switch (additionalItems.type()) { // items is an array
// we need to take into consideration additionalItems
case json::value_t::object:
validate(value, additionalItems, sub_name);
break;
case json::value_t::boolean:
if (additionalItems == false)
throw std::out_of_range("additional values in array are not allowed for " + sub_name);
else
validation_done = true;
break;
default:
break;
}
}
break;
case json::value_t::object: // items is a schema
validate(value, items, sub_name);
break;
default:
break;
}
if (validation_done)
break;
i++;
}
}
void json_validator::validate_object(json &instance, const json &schema, const std::string &name)
{
validate_type(schema, "object", name);
json properties = {};
if (schema.find("properties") != schema.end())
properties = schema["properties"];
// check for default values of properties
// and insert them into this object, if they don't exists
// works only for object properties for the moment
if (default_value_insertion)
for (auto it = properties.begin(); it != properties.end(); ++it) {
const auto &default_value = it.value().find("default");
if (default_value == it.value().end())
continue; /* no default value -> continue */
if (instance.find(it.key()) != instance.end())
continue; /* value is present */
/* create element from default value */
instance[it.key()] = default_value.value();
}
// maxProperties
const auto &maxProperties = schema.find("maxProperties");
if (maxProperties != schema.end())
if (instance.size() > maxProperties.value())
throw std::out_of_range(name + " has too many properties.");
// minProperties
const auto &minProperties = schema.find("minProperties");
if (minProperties != schema.end())
if (instance.size() < minProperties.value())
throw std::out_of_range(name + " has too few properties.");
// additionalProperties
enum {
True,
False,
Object
} additionalProperties = True;
const auto &additionalPropertiesVal = schema.find("additionalProperties");
if (additionalPropertiesVal != schema.end()) {
if (additionalPropertiesVal.value().type() == json::value_t::boolean)
additionalProperties = additionalPropertiesVal.value() == true ? True : False;
else
additionalProperties = Object;
}
// patternProperties
json patternProperties = {};
if (schema.find("patternProperties") != schema.end())
patternProperties = schema["patternProperties"];
// check all elements in object
for (auto child = instance.begin(); child != instance.end(); ++child) {
std::string child_name = name + "." + child.key();
// is this a property which is described in the schema
const auto &object_prop = properties.find(child.key());
if (object_prop != properties.end()) {
// validate the element with its schema
validate(child.value(), object_prop.value(), child_name);
continue;
}
bool patternProperties_has_matched = false;
for (auto pp = patternProperties.begin();
pp != patternProperties.end(); ++pp) {
std::regex re(pp.key(), std::regex::ECMAScript);
if (std::regex_search(child.key(), re)) {
validate(child.value(), pp.value(), child_name);
patternProperties_has_matched = true;
}
}
if (patternProperties_has_matched)
continue;
switch (additionalProperties) {
case True:
break;
case Object:
validate(child.value(), additionalPropertiesVal.value(), child_name);
break;
case False:
throw std::invalid_argument("unknown property '" + child.key() + "' in object '" + name + "'");
break;
};
}
// required
const auto &required = schema.find("required");
if (required != schema.end())
for (const auto &element : required.value()) {
if (instance.find(element) == instance.end()) {
throw std::invalid_argument("required element '" + element.get<std::string>() +
"' not found in object '" + name + "'");
}
}
// dependencies
const auto &dependencies = schema.find("dependencies");
if (dependencies == schema.end())
return;
for (auto dep = dependencies.value().cbegin();
dep != dependencies.value().cend();
++dep) {
// property not present in this instance - next
if (instance.find(dep.key()) == instance.end())
continue;
std::string sub_name = name + ".dependency-of-" + dep.key();
switch (dep.value().type()) {
case json::value_t::object:
validate(instance, dep.value(), sub_name);
break;
case json::value_t::array:
for (const auto &prop : dep.value())
if (instance.find(prop) == instance.end())
throw std::invalid_argument("failed dependency for " + sub_name + ". Need property " + prop.get<std::string>());
break;
default:
break;
}
}
}
void json_validator::validate(json &instance, const json &schema_, const std::string &name)
{
not_yet_implemented(schema_, "allOf", "all");
not_yet_implemented(schema_, "anyOf", "all");
not_yet_implemented(schema_, "oneOf", "all");
not_yet_implemented(schema_, "not", "all");
const json *schema = &schema_;
do {
const auto &ref = schema->find("$ref");
if (ref != schema->end()) {
auto it = schema_refs_.find(ref.value());
if (it == schema_refs_.end())
throw std::invalid_argument("schema reference " + ref.value().get<std::string>() + " not found. Make sure all schemas have been inserted before validation.");
schema = it->second;
} else
break;
} while (1); // loop in case of nested refs
validate_enum(instance, *schema, name);
switch (instance.type()) {
case json::value_t::object:
validate_object(instance, *schema, name);
break;
case json::value_t::array:
validate_array(instance, *schema, name);
break;
case json::value_t::string:
validate_string(instance, *schema, name);
break;
case json::value_t::number_unsigned:
validate_unsigned(instance, *schema, name);
break;
case json::value_t::number_integer:
validate_integer(instance, *schema, name);
break;
case json::value_t::number_float:
validate_float(instance, *schema, name);
break;
case json::value_t::boolean:
validate_boolean(instance, *schema, name);
break;
case json::value_t::null:
validate_null(instance, *schema, name);
break;
default:
assert(0 && "unexpected instance type for validation");
break;
}
}
void json_validator::validate(json &instance)
{
if (root_schema_ == nullptr)
throw std::invalid_argument("no root-schema has been inserted. Cannot validate an instance without it.");
validate(instance, *root_schema_, "root");
}
}
}

29
test/id.schema.json Normal file
View File

@ -0,0 +1,29 @@
{
"id": "http://example.com/root.json",
"definitions": {
"A": { "id": "#foo" },
"B": {
"id": "other.json",
"definitions": {
"X": { "id": "#bar" },
"Y": { "id": "t/inner.json" }
}
},
"C": {
"id": "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f"
},
"single": {
"id": "#item",
"type": "integer"
},
"a": {"type": "integer"},
"b": {"$ref": "#/definitions/a"},
"c": {"$ref": "#/definitions/b"},
"remote": { "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" }
},
"items": {
"type": "array",
"items": [ { "$ref": "#item" }, { "$ref": "other.json#bar" } ]
}
}