diff --git a/CMakeLists.txt b/CMakeLists.txt index 8300b74..fbf0331 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,15 +21,18 @@ if(NOT TARGET json-hpp) endif() # 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 - INTERFACE + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) target_compile_options(json-schema-validator - INTERFACE + PUBLIC -Wall -Wextra) # bad, better use something else based on compiler type target_link_libraries(json-schema-validator - INTERFACE + PUBLIC json-hpp) install( @@ -43,10 +46,6 @@ install( add_executable(json-schema-validate app/json-schema-validate.cpp) 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 enable_testing() @@ -56,6 +55,14 @@ find_path(JSON_SCHEMA_TEST_SUITE_PATH tests/draft4) 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 file(GLOB_RECURSE TEST_FILES ${JSON_SCHEMA_TEST_SUITE_PATH}/tests/draft4/*.json) diff --git a/README.md b/README.md index 1a432b2..a5643e1 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ int main(void) 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). -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 require a rework. Some will only work with external libraries. (remote references) diff --git a/app/json-schema-test.cpp b/app/json-schema-test.cpp index 8a91a3d..7bb7ce9 100644 --- a/app/json-schema-test.cpp +++ b/app/json-schema-test.cpp @@ -1,11 +1,48 @@ -#include "json-schema-validator.hpp" +/* + * Modern C++ JSON schema validator + * + * Licensed under the MIT License . + * + * Copyright (c) 2016 Patrick Boettcher . + * + * 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 using nlohmann::json; -using nlohmann::json_validator; +using nlohmann::json_uri; +using nlohmann::json_schema_draft4::json_validator; int main(void) { - json validation; + json validation; // a validation case following the JSON-test-suite-schema + + std::map 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 { std::cin >> validation; @@ -25,22 +62,64 @@ int main(void) const auto &schema = test_group["schema"]; + json_validator validator; + do { + std::set 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"]) { std::cout << " Testing Case " << test_case["description"] << "\n"; bool valid = true; try { - json_validator validator; - validator.set_schema("#", schema); validator.validate(test_case["data"]); } catch (const std::out_of_range &e) { + valid = false; std::cout << " Test Case Exception (out of range): " << e.what() << "\n"; + } catch (const std::invalid_argument &e) { + valid = false; std::cout << " Test Case Exception (invalid argument): " << e.what() << "\n"; + } catch (const std::logic_error &e) { + valid = !test_case["valid"]; /* force test-case failure */ std::cout << " Not yet implemented: " << e.what() << "\n"; } diff --git a/app/json-schema-validate.cpp b/app/json-schema-validate.cpp index 5eb5aaa..c0b0260 100644 --- a/app/json-schema-validate.cpp +++ b/app/json-schema-validate.cpp @@ -1,31 +1,66 @@ -#include "json-schema-validator.hpp" +/* + * Modern C++ JSON schema validator + * + * Licensed under the MIT License . + * + * Copyright (c) 2016 Patrick Boettcher . + * + * 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 #include - -#include +#include using nlohmann::json; -using nlohmann::json_validator; +using nlohmann::json_uri; +using nlohmann::json_schema_draft4::json_validator; static void usage(const char *name) { - std::cerr << "Usage: " << name << " < \n"; + std::cerr << "Usage: " << name << " < \n"; 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[]) { if (argc != 2) usage(argv[0]); + json_validator validator; + std::fstream f(argv[1]); if (!f.good()) { std::cerr << "could not open " << argv[1] << " for reading\n"; usage(argv[0]); } + // 1) Read the schema for the document you want to validate json schema; - try { f >> schema; } catch (std::exception &e) { @@ -33,19 +68,46 @@ int main(int argc, char *argv[]) return EXIT_FAILURE; } + // 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 { + lf >> extra; + } catch (std::exception &e) { + std::cerr << e.what() << " at " << lf.tellp() << "\n"; + 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 { - std::cin >> document; - } catch (std::exception &e) { - std::cerr << e.what() << " at " << f.tellp() << "\n"; - return EXIT_FAILURE; - } - - - try { - json_validator validator; - validator.set_schema("#", schema); + document << std::cin; validator.validate(document); } catch (std::exception &e) { std::cerr << "schema validation failed\n"; @@ -53,7 +115,7 @@ int main(int argc, char *argv[]) return EXIT_FAILURE; } - std::cerr << std::setw(2) << document << "\n"; + std::cerr << "document is valid\n"; return EXIT_SUCCESS; } diff --git a/src/json-schema-draft4.json.cpp b/src/json-schema-draft4.json.cpp new file mode 100644 index 0000000..6cf9fee --- /dev/null +++ b/src/json-schema-draft4.json.cpp @@ -0,0 +1,157 @@ +#include + +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; + +} diff --git a/src/json-schema-validator.hpp b/src/json-schema-validator.hpp deleted file mode 100644 index a6e9fe8..0000000 --- a/src/json-schema-validator.hpp +++ /dev/null @@ -1,548 +0,0 @@ -/* - * Modern C++ JSON schema validator - * - * Licensed under the MIT License . - * - * Copyright (c) 2016 Patrick Boettcher . - * - * 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 - -#include -#include - -// 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() + " 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().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().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 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() + - "' 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()); - 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 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__ */ diff --git a/src/json-schema.hpp b/src/json-schema.hpp new file mode 100644 index 0000000..3137353 --- /dev/null +++ b/src/json-schema.hpp @@ -0,0 +1,201 @@ +/* + * Modern C++ JSON schema validator + * + * Licensed under the MIT License . + * + * Copyright (c) 2016 Patrick Boettcher . + * + * 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 + +#include + +// 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 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> schema_store_; + std::shared_ptr root_schema_; + + std::map 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 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__ */ diff --git a/src/json-uri.cpp b/src/json-uri.cpp new file mode 100644 index 0000000..72c88da --- /dev/null +++ b/src/json-uri.cpp @@ -0,0 +1,127 @@ +/* + * Modern C++ JSON schema validator + * + * Licensed under the MIT License . + * + * Copyright (c) 2016 Patrick Boettcher . + * + * 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(); +} + +} diff --git a/src/json-validator.cpp b/src/json-validator.cpp new file mode 100644 index 0000000..6e8752e --- /dev/null +++ b/src/json-validator.cpp @@ -0,0 +1,610 @@ +/* + * Modern C++ JSON schema validator + * + * Licensed under the MIT License . + * + * Copyright (c) 2016 Patrick Boettcher . + * + * 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 + +#include +#include + +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 refs; + +public: + std::set undefined_refs; + + std::map 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_validator::insert_schema(const json &input, json_uri id) +{ + // allocate create a copy for later storage - if resolving reference works + std::shared_ptr schema = std::make_shared(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 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() + " 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().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().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 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() + + "' 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()); + 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() + " 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"); +} +} +} diff --git a/test/id.schema.json b/test/id.schema.json new file mode 100644 index 0000000..dda0e09 --- /dev/null +++ b/test/id.schema.json @@ -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" } ] + } +}