diff --git a/.clang-format b/.clang-format
index 647604d..715ac57 100644
--- a/.clang-format
+++ b/.clang-format
@@ -7,9 +7,11 @@ AllowShortFunctionsOnASingleLine: Inline
BreakBeforeBraces: Linux
ColumnLimit: 0
ConstructorInitializerAllOnOneLineOrOnePerLine: true
-IndentWidth: 2
-ObjCBlockIndentWidth: 2
+IndentWidth: 4
+IndentPPDirectives: AfterHash
+ObjCBlockIndentWidth: 0
SpaceAfterCStyleCast: true
-TabWidth: 2
+TabWidth: 4
+AccessModifierOffset: -4
UseTab: ForIndentation
...
diff --git a/.travis.yml b/.travis.yml
index e4ee62f..edb2981 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -60,7 +60,7 @@ script:
- $CXX --version
# put json.hpp to nlohmann
- - mkdir -p nlohmann && wget https://github.com/nlohmann/json/releases/download/v3.1.2/json.hpp -O nlohmann/json.hpp
+ - mkdir -p nlohmann && wget https://github.com/nlohmann/json/releases/download/v3.5.0/json.hpp -O nlohmann/json.hpp
# compile and execute unit tests
- mkdir -p build && cd build
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b2a77e7..e407979 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -32,7 +32,7 @@ endif()
# and one for the validator
add_library(json-schema-validator
- src/json-schema-draft4.json.cpp
+ src/json-schema-draft7.json.cpp
src/json-uri.cpp
src/json-validator.cpp)
@@ -95,8 +95,13 @@ if (BUILD_EXAMPLES)
# simple json-schema-validator-executable
add_executable(json-schema-validate app/json-schema-validate.cpp)
target_link_libraries(json-schema-validate json-schema-validator)
+
+ add_executable(readme app/readme.cpp)
+ target_link_libraries(readme json-schema-validator)
endif()
+#add_subdirectory(ng)
+
if (BUILD_TESTS)
# test-zone
enable_testing()
diff --git a/README.md b/README.md
index a1a1f95..691af12 100644
--- a/README.md
+++ b/README.md
@@ -1,53 +1,68 @@
[](https://travis-ci.org/pboettch/json-schema-validator)
-# Modern C++ JSON schema validator
+# JSON schema validator for JSON for Modern C++
# What is it?
This is a C++ library for validating JSON documents based on a
[JSON Schema](http://json-schema.org/) which itself should validate with
-[draft-4 of JSON Schema Validation](http://json-schema.org/schema).
+[draft-7 of JSON Schema Validation](http://json-schema.org/schema).
First a disclaimer: *It is work in progress and
-contributions or hints or discussions are welcome.*
+contributions or hints or discussions are welcome.* Even though a 2.0.0 release is immenent.
Niels Lohmann et al develop a great JSON parser for C++ called [JSON for Modern
C++](https://github.com/nlohmann/json). This validator is based on this
library, hence the name.
-The name is for the moment purely marketing, because there is, IMHO, not so much
-modern C++ inside. There is plenty of space to make it more modern.
-
External documentation is missing as well. However the API of the validator
-will be rather simple.
+is rather simple.
+
+# New in version 2
+
+Although significant changes have been coorporate to the 2 version
+(a complete rewrite) the API is compatible with the 1.0.0 release. Except for
+the namespace which is now `nlohmann::json_schema.
+
+Version **2** supports JSON schema draft 7, whereas 1 was supporting draft 4
+only. Please update your schemas.
+
+The primary change in 2 is the way a schema is used. While in version 1 the schema was
+kept as a JSON-document and used again and again during validation, in versin 2 the schema
+is parsed into compiled C++ objects which are then used during validation. There are surely
+still optimizations to be done, but validation speed has improved by factor 100
+or more.
+
+In JSON-schema one sub-schema can be
# Design goals
The main goal of this validator is to produce *human-comprehensible* error
-messages if a JSON-document/instance does not comply with its schema. This is
-done with exceptions thrown at the users with a helpful message telling what's
-wrong with the document while validating.
+messages if a JSON-document/instance does not comply to its schema.
+
+By default this is done with exceptions thrown at the users with a helpful
+message telling what's wrong with the document while validating.
+
+With **2.0.0** the user can passed a `json_scheam::basic_error_handler` derived object
+along with the instance to validate to receive a each time a validation error occurs
+and decice what to do (throwing, counting, collecting).
Another goal was to use Niels Lohmann's JSON-library. This is why the validator
lives in his namespace.
# Weaknesses
-Schema-reference resolution is not recursivity-proven: If there is a nested
-cross-schema reference, it will not stop. (Though I haven't tested it)
-
-Numerical validation uses `int64_t`, `uint64_t` or `double`, depending on if
+Numerical validation uses nlohmann integer, unsigned and floating point types, depending on if
the schema type is "integer" or "number". Bignum (i.e. arbitrary precision and
range) is not supported at this time.
-Unsigned integer validation will only take place if the following two conditions are true:
-- The nlohmann `type()` of the json object under validation is `nlohmann::json::value_t::number_unsigned`
-- The schema specifies a numerical minimum greater than or equal to 0
+Currently JSON-URI with "plain name fragments" are not supported. So referring to an URI
+with `$ref: "file.json#plain"` will not work.
# How to use
-The current state of the build-system needs at least version **3.1.1** of NLohmann's
+The current state of the build-system needs at least version **3.5.0** of NLohmann's
JSON library. It is looking for the `json.hpp` within a `nlohmann/`-path.
When build the library you need to provide the path to the directory where the include-file
@@ -66,7 +81,7 @@ cmake .. \
-DNLOHMANN_JSON_DIR=nlohmann/json.hpp \
-DJSON_SCHEMA_TEST_SUITE_PATH= # optional
make # install
-ctest # if test-suite has been given
+ctest # run unit, non-regression and test-suite tests
```
### As a subdirectory from within
@@ -93,7 +108,6 @@ In your initial call to cmake simply add:
```bash
cmake -DBUILD_SHARED_LIBS=ON
```
-
## Code
See also `app/json-schema-validate.cpp`.
@@ -104,13 +118,12 @@ See also `app/json-schema-validate.cpp`.
#include "json-schema.hpp"
using nlohmann::json;
-using nlohmann::json_uri;
-using nlohmann::json_schema_draft4::json_validator;
+using nlohmann::json_schema::json_validator;
// The schema is defined based upon a string literal
static json person_schema = R"(
{
- "$schema": "http://json-schema.org/draft-04/schema#",
+ "$schema": "http://json-schema.org/draft-07/schema#",
"title": "A person",
"properties": {
"name": {
@@ -137,55 +150,74 @@ static json person_schema = R"(
static json bad_person = {{"age", 42}};
static json good_person = {{"name", "Albert"}, {"age", 42}};
-int main(){
+int main()
+{
+ /* json-parse the schema */
- /* json-parse the schema */
+ json_validator validator; // create validator
- json_validator validator; // create validator
+ try {
+ validator.set_root_schema(person_schema); // insert root-schema
+ } catch (const std::exception &e) {
+ std::cerr << "Validation of schema failed, here is why: " << e.what() << "\n";
+ return EXIT_FAILURE;
+ }
- try {
- validator.set_root_schema(person_schema); // insert root-schema
- } catch (const std::exception &e) {
- std::cerr << "Validation of schema failed, here is why: " << e.what() << "\n";
- return EXIT_FAILURE;
- }
+ /* json-parse the people - API of 1.0.0, default throwing error handler */
- /* json-parse the people */
+ for (auto &person : {bad_person, good_person}) {
+ std::cout << "About to validate this person:\n"
+ << std::setw(2) << person << std::endl;
+ try {
+ validator.validate(person); // validate the document
+ std::cout << "Validation succeeded\n";
+ } catch (const std::exception &e) {
+ std::cerr << "Validation failed, here is why: " << e.what() << "\n";
+ }
+ }
- for (auto &person : {bad_person, good_person})
- {
- std::cout << "About to validate this person:\n" << std::setw(2) << person << std::endl;
- try {
- validator.validate(person); // validate the document
- std::cout << "Validation succeeded\n";
- } catch (const std::exception &e) {
- std::cerr << "Validation failed, here is why: " << e.what() << "\n";
- }
- }
- return EXIT_SUCCESS;
+ /* json-parse the people - with custom error handler */
+ class custom_error_handler : public nlohmann::json_schema::basic_error_handler
+ {
+ void error(const std::string &path, const json &instance, const std::string &message) override
+ {
+ nlohmann::json_schema::basic_error_handler::error(path, instance, message);
+ std::cerr << "ERROR: '" << path << "' - '" << instance << "': " << message << "\n";
+ }
+ };
+
+
+ for (auto &person : {bad_person, good_person}) {
+ std::cout << "About to validate this person:\n"
+ << std::setw(2) << person << std::endl;
+
+ custom_error_handler err;
+ validator.validate(person, err); // validate the document - uses the default throwing error-handler
+
+ if (err)
+ std::cerr << "Validation failed\n";
+ else
+ std::cout << "Validation succeeded\n";
+ }
+
+ return EXIT_SUCCESS;
}
-
```
# Compliance
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).
+In order to simplify the testing, the test-suite is included in the repository.
If you have cloned this repository providing a path the repository-root via the
cmake-variable `JSON_SCHEMA_TEST_SUITE_PATH` will enable the test-target(s).
All required tests are **OK**.
-**12** optional tests of **305** total (required + optional) tests are failing:
-
-- 10 of them are `format`-strings which are not supported.
-- big numbers are not working (2)
-
# Additional features
## Default values
The goal is to create an empty document, based on schema-defined
default-values, recursively populated.
-
diff --git a/app/json-schema-validate.cpp b/app/json-schema-validate.cpp
index 055f19b..b6699e3 100644
--- a/app/json-schema-validate.cpp
+++ b/app/json-schema-validate.cpp
@@ -1,27 +1,10 @@
/*
- * Modern C++ JSON schema validator
+ * JSON schema validator for JSON for modern C++
*
- * Licensed under the MIT License .
+ * Copyright (c) 2016-2019 Patrick Boettcher
.
*
- * Copyright (c) 2016 Patrick Boettcher .
+ * SPDX-License-Identifier: MIT
*
- * 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
@@ -30,7 +13,7 @@
using nlohmann::json;
using nlohmann::json_uri;
-using nlohmann::json_schema_draft4::json_validator;
+using nlohmann::json_schema::json_validator;
static void usage(const char *name)
{
@@ -47,9 +30,10 @@ static void usage(const char *name)
static void loader(const json_uri &uri, json &schema)
{
- std::fstream lf("." + uri.path());
+ std::string filename = "./" + uri.path();
+ std::fstream lf(filename);
if (!lf.good())
- throw std::invalid_argument("could not open " + uri.url() + " tried with " + uri.path());
+ throw std::invalid_argument("could not open " + uri.url() + " tried with " + filename);
try {
lf >> schema;
@@ -58,6 +42,15 @@ static void loader(const json_uri &uri, json &schema)
}
}
+class custom_error_handler : public nlohmann::json_schema::basic_error_handler
+{
+ void error(const std::string &path, const json &instance, const std::string &message) override
+ {
+ nlohmann::json_schema::basic_error_handler::error(path, instance, message);
+ std::cerr << "ERROR: '" << path << "' - '" << instance << "': " << message << "\n";
+ }
+};
+
int main(int argc, char *argv[])
{
if (argc != 2)
@@ -95,10 +88,16 @@ int main(int argc, char *argv[])
try {
std::cin >> document;
- validator.validate(document);
} catch (std::exception &e) {
+ std::cerr << "json parsing failed: " << e.what() << " at offset: " << std::cin.tellg() << "\n";
+ return EXIT_FAILURE;
+ }
+
+ custom_error_handler err;
+ validator.validate(document, err);
+
+ if (err) {
std::cerr << "schema validation failed\n";
- std::cerr << e.what() << " at offset: " << std::cin.tellg() << "\n";
return EXIT_FAILURE;
}
diff --git a/app/readme.cpp b/app/readme.cpp
new file mode 100644
index 0000000..a25d652
--- /dev/null
+++ b/app/readme.cpp
@@ -0,0 +1,90 @@
+#include
+#include
+
+#include "json-schema.hpp"
+
+using nlohmann::json;
+using nlohmann::json_schema::json_validator;
+
+// The schema is defined based upon a string literal
+static json person_schema = R"(
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "A person",
+ "properties": {
+ "name": {
+ "description": "Name",
+ "type": "string"
+ },
+ "age": {
+ "description": "Age of the person",
+ "type": "number",
+ "minimum": 2,
+ "maximum": 200
+ }
+ },
+ "required": [
+ "name",
+ "age"
+ ],
+ "type": "object"
+}
+
+)"_json;
+
+// The people are defined with brace initialization
+static json bad_person = {{"age", 42}};
+static json good_person = {{"name", "Albert"}, {"age", 42}};
+
+int main()
+{
+ /* json-parse the schema */
+
+ json_validator validator; // create validator
+
+ try {
+ validator.set_root_schema(person_schema); // insert root-schema
+ } catch (const std::exception &e) {
+ std::cerr << "Validation of schema failed, here is why: " << e.what() << "\n";
+ return EXIT_FAILURE;
+ }
+
+ /* json-parse the people - API of 1.0.0, default throwing error handler */
+
+ for (auto &person : {bad_person, good_person}) {
+ std::cout << "About to validate this person:\n"
+ << std::setw(2) << person << std::endl;
+ try {
+ validator.validate(person); // validate the document
+ std::cout << "Validation succeeded\n";
+ } catch (const std::exception &e) {
+ std::cerr << "Validation failed, here is why: " << e.what() << "\n";
+ }
+ }
+
+ /* json-parse the people - with custom error handler */
+ class custom_error_handler : public nlohmann::json_schema::basic_error_handler
+ {
+ void error(const std::string &path, const json &instance, const std::string &message) override
+ {
+ nlohmann::json_schema::basic_error_handler::error(path, instance, message);
+ std::cerr << "ERROR: '" << path << "' - '" << instance << "': " << message << "\n";
+ }
+ };
+
+
+ for (auto &person : {bad_person, good_person}) {
+ std::cout << "About to validate this person:\n"
+ << std::setw(2) << person << std::endl;
+
+ custom_error_handler err;
+ validator.validate(person, err); // validate the document - uses the default throwing error-handler
+
+ if (err)
+ std::cerr << "Validation failed\n";
+ else
+ std::cout << "Validation succeeded\n";
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/json-schema-draft4.json.cpp b/schema
similarity index 57%
rename from src/json-schema-draft4.json.cpp
rename to schema
index 43bab6f..5bee90e 100644
--- a/src/json-schema-draft4.json.cpp
+++ b/schema
@@ -1,109 +1,114 @@
-#include
-
-namespace nlohmann
{
-namespace 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",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "http://json-schema.org/draft-07/schema#",
+ "title": "Core schema meta-schema",
"definitions": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#" }
},
- "positiveInteger": {
+ "nonNegativeInteger": {
"type": "integer",
"minimum": 0
},
- "positiveIntegerDefault0": {
- "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]
+ "nonNegativeIntegerDefault0": {
+ "allOf": [
+ { "$ref": "#/definitions/nonNegativeInteger" },
+ { "default": 0 }
+ ]
},
"simpleTypes": {
- "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
+ "enum": [
+ "array",
+ "boolean",
+ "integer",
+ "null",
+ "number",
+ "object",
+ "string"
+ ]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
- "minItems": 1,
- "uniqueItems": true
+ "uniqueItems": true,
+ "default": []
}
},
- "type": "object",
+ "type": ["object", "boolean"],
"properties": {
- "id": {
+ "$id": {
"type": "string",
- "format": "uri"
+ "format": "uri-reference"
},
"$schema": {
"type": "string",
"format": "uri"
},
+ "$ref": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "$comment": {
+ "type": "string"
+ },
"title": {
"type": "string"
},
"description": {
"type": "string"
},
- "default": {},
+ "default": true,
+ "readOnly": {
+ "type": "boolean",
+ "default": false
+ },
+ "examples": {
+ "type": "array",
+ "items": true
+ },
"multipleOf": {
"type": "number",
- "minimum": 0,
- "exclusiveMinimum": true
+ "exclusiveMinimum": 0
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
- "type": "boolean",
- "default": false
+ "type": "number"
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
- "type": "boolean",
- "default": false
+ "type": "number"
},
- "maxLength": { "$ref": "#/definitions/positiveInteger" },
- "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
+ "maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
+ "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
- "additionalItems": {
- "anyOf": [
- { "type": "boolean" },
- { "$ref": "#" }
- ],
- "default": {}
- },
+ "additionalItems": { "$ref": "#" },
"items": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/schemaArray" }
],
- "default": {}
+ "default": true
},
- "maxItems": { "$ref": "#/definitions/positiveInteger" },
- "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
+ "maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
+ "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
- "maxProperties": { "$ref": "#/definitions/positiveInteger" },
- "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
+ "contains": { "$ref": "#" },
+ "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
+ "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"required": { "$ref": "#/definitions/stringArray" },
- "additionalProperties": {
- "anyOf": [
- { "type": "boolean" },
- { "$ref": "#" }
- ],
- "default": {}
- },
+ "additionalProperties": { "$ref": "#" },
"definitions": {
"type": "object",
"additionalProperties": { "$ref": "#" },
@@ -117,6 +122,7 @@ json draft4_schema_builtin = R"( {
"patternProperties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
+ "propertyNames": { "format": "regex" },
"default": {}
},
"dependencies": {
@@ -128,8 +134,11 @@ json draft4_schema_builtin = R"( {
]
}
},
+ "propertyNames": { "$ref": "#" },
+ "const": true,
"enum": {
"type": "array",
+ "items": true,
"minItems": 1,
"uniqueItems": true
},
@@ -144,17 +153,16 @@ json draft4_schema_builtin = R"( {
}
]
},
+ "format": { "type": "string" },
+ "contentMediaType": { "type": "string" },
+ "contentEncoding": { "type": "string" },
+ "if": {"$ref": "#"},
+ "then": {"$ref": "#"},
+ "else": {"$ref": "#"},
"allOf": { "$ref": "#/definitions/schemaArray" },
"anyOf": { "$ref": "#/definitions/schemaArray" },
"oneOf": { "$ref": "#/definitions/schemaArray" },
"not": { "$ref": "#" }
},
- "dependencies": {
- "exclusiveMaximum": [ "maximum" ],
- "exclusiveMinimum": [ "minimum" ]
- },
- "default": {}
-} )"_json;
-
-}
+ "default": true
}
diff --git a/src/json-schema-draft7.json.cpp b/src/json-schema-draft7.json.cpp
new file mode 100644
index 0000000..b680e2c
--- /dev/null
+++ b/src/json-schema-draft7.json.cpp
@@ -0,0 +1,185 @@
+/*
+ * JSON schema validator for JSON for modern C++
+ *
+ * Copyright (c) 2016-2019 Patrick Boettcher
.
*
- * Copyright (c) 2016 Patrick Boettcher .
+ * SPDX-License-Identifier: MIT
*
- * 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__
#ifdef _WIN32
-# if defined(JSON_SCHEMA_VALIDATOR_EXPORTS)
-# define JSON_SCHEMA_VALIDATOR_API __declspec(dllexport)
-# elif defined(JSON_SCHEMA_VALIDATOR_IMPORTS)
-# define JSON_SCHEMA_VALIDATOR_API __declspec(dllimport)
-# else
-# define JSON_SCHEMA_VALIDATOR_API
-# endif
+# if defined(JSON_SCHEMA_VALIDATOR_EXPORTS)
+# define JSON_SCHEMA_VALIDATOR_API __declspec(dllexport)
+# elif defined(JSON_SCHEMA_VALIDATOR_IMPORTS)
+# define JSON_SCHEMA_VALIDATOR_API __declspec(dllimport)
+# else
+# define JSON_SCHEMA_VALIDATOR_API
+# endif
#else
-# define JSON_SCHEMA_VALIDATOR_API
+# define JSON_SCHEMA_VALIDATOR_API
#endif
#include
+#ifdef NLOHMANN_JSON_VERSION_MAJOR
+# if NLOHMANN_JSON_VERSION_MAJOR < 3 || NLOHMANN_JSON_VERSION_MINOR < 5 || NLOHMANN_JSON_VERSION_PATCH < 0
+# error "Please use this library with NLohmann's JSON version 3.5.0 or higher"
+# endif
+#else
+# error "expected existing NLOHMANN_JSON_VERSION_MAJOR preproc variable, please update to NLohmann's JSON 3.5.0"
+#endif
+
// 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 local_json_pointer
-{
- std::string str_;
-
- void from_string(const std::string &r);
-
-public:
- local_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
@@ -102,32 +51,32 @@ class JSON_SCHEMA_VALIDATOR_API json_uri
std::string proto_;
std::string hostname_;
std::string path_;
- local_json_pointer pointer_;
+ nlohmann::json::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);
+ void update(const std::string &uri);
std::tuple tie() const
{
- return std::tie(urn_, proto_, hostname_, path_, pointer_.to_string());
+ return std::tie(urn_, proto_, hostname_, path_, pointer_);
}
public:
json_uri(const std::string &uri)
{
- from_string(uri);
+ update(uri);
}
const std::string protocol() const { return proto_; }
const std::string hostname() const { return hostname_; }
const std::string path() const { return path_; }
- const local_json_pointer pointer() const { return pointer_; }
- const std::string url() const;
+ const nlohmann::json::json_pointer pointer() const { return pointer_; }
+
+ const std::string url() const { return location(); }
+ const std::string location() const;
- // decode and encode strings for ~ and % escape sequences
- static std::string unescape(const std::string &);
static std::string escape(const std::string &);
// create a new json_uri based in this one and the given uri
@@ -135,7 +84,7 @@ public:
json_uri derive(const std::string &uri) const
{
json_uri u = *this;
- u.from_string(uri);
+ u.update(uri);
return u;
}
@@ -143,7 +92,7 @@ public:
json_uri append(const std::string &field) const
{
json_uri u = *this;
- u.pointer_.append("/" + field);
+ u.pointer_ = nlohmann::json::json_pointer(u.pointer_.to_string() + '/' + escape(field));
return u;
}
@@ -162,42 +111,47 @@ public:
friend std::ostream &operator<<(std::ostream &os, const json_uri &u);
};
-namespace json_schema_draft4
+namespace json_schema
{
-extern json draft4_schema_builtin;
+extern json draft7_schema_builtin;
+
+class basic_error_handler
+{
+ bool error_{false};
+
+public:
+ virtual void error(const std::string & /*path*/, const json & /* instance */, const std::string & /*message*/)
+ {
+ error_ = true;
+ }
+
+ void reset() { error_ = false; }
+ operator bool() const { return error_; }
+};
+
+class root_schema;
class JSON_SCHEMA_VALIDATOR_API json_validator
{
- std::vector> schema_store_;
- std::shared_ptr root_schema_;
- std::function schema_loader_ = nullptr;
- std::function format_check_ = nullptr;
-
- std::map schema_refs_;
-
- void validate(const json &instance, const json &schema_, const std::string &name);
- void validate_array(const json &instance, const json &schema_, const std::string &name);
- void validate_object(const json &instance, const json &schema_, const std::string &name);
- void validate_string(const json &instance, const json &schema, const std::string &name);
-
- void insert_schema(const json &input, const json_uri &id);
+ std::unique_ptr root_;
public:
json_validator(std::function loader = nullptr,
- std::function format = nullptr)
- : schema_loader_(loader), format_check_(format)
- {
- }
+ std::function format = nullptr);
+ ~json_validator();
- // insert and set a root-schema
+ // insert and set thea root-schema
void set_root_schema(const json &);
// validate a json-document based on the root-schema
- void validate(const json &instance);
+ void validate(const json &);
+
+ // validate a json-document based on the root-schema with a custom error-handler
+ void validate(const json &, basic_error_handler &);
};
-} // json_schema_draft4
-} // nlohmann
+} // namespace json_schema
+} // namespace nlohmann
#endif /* NLOHMANN_JSON_SCHEMA_HPP__ */
diff --git a/src/json-uri.cpp b/src/json-uri.cpp
index 6a3136f..f8d6cbe 100644
--- a/src/json-uri.cpp
+++ b/src/json-uri.cpp
@@ -1,27 +1,10 @@
/*
- * Modern C++ JSON schema validator
+ * JSON schema validator for JSON for modern C++
*
- * Licensed under the MIT License .
+ * Copyright (c) 2016-2019 Patrick Boettcher
.
*
- * Copyright (c) 2016 Patrick Boettcher .
+ * SPDX-License-Identifier: MIT
*
- * 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"
@@ -30,76 +13,92 @@
namespace nlohmann
{
-void local_json_pointer::from_string(const std::string &r)
+void json_uri::update(const std::string &uri)
{
- str_ = "#";
+ std::string pointer = ""; // default pointer is document-root
- 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
+ // first split the URI into location and 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);
+ if (pointer_separator != std::string::npos) { // and extract the pointer-string if found
+ pointer = uri.substr(pointer_separator + 1); // remove #
- // the rest is an URL
- std::string url = uri.substr(0, pointer_separator);
- if (url.size()) { // if an URL is part of the URI
+ // unescape %-values IOW, decode JSON-URI-formatted JSON-pointer
+ std::size_t pos = pointer.size() - 1;
+ do {
+ pos = pointer.rfind('%', pos);
+ if (pos == std::string::npos)
+ break;
- 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;
+ if (pos >= pointer.size() - 2) {
+ pos--;
+ continue;
}
- }
- // 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);
+ std::string hex = pointer.substr(pos + 1, 2);
+ char ascii = (char) std::strtoul(hex.c_str(), nullptr, 16);
+ pointer.replace(pos, 3, 1, ascii);
- pointer_ = local_json_pointer("");
+ pos--;
+ } while (1);
}
- if (pointer.size() > 0)
- pointer_ = pointer;
+ auto location = uri.substr(0, pointer_separator);
+
+ if (location.size()) { // a location part has been found
+ pointer_ = ""_json_pointer; // if a location is given, the pointer is emptied
+
+ // if it is an URN take it as it is
+ if (location.find("urn:") == 0) {
+ urn_ = location;
+
+ // and clear URL members
+ proto_ = "";
+ hostname_ = "";
+ path_ = "";
+
+ } else { // it is an URL
+
+ // split URL in protocol, hostname and path
+ std::size_t pos = 0;
+ auto proto = location.find("://", pos);
+ if (proto != std::string::npos) { // extract the protocol
+
+ urn_ = ""; // clear URN-member if URL is parsed
+
+ proto_ = location.substr(pos, proto - pos);
+ pos = 3 + proto; // 3 == "://"
+
+ auto hostname = location.find("/", pos);
+ if (hostname != std::string::npos) { // and the hostname (no proto without hostname)
+ hostname_ = location.substr(pos, hostname - pos);
+ pos = hostname;
+ }
+ }
+
+ auto path = location.substr(pos);
+
+ // URNs cannot of have paths
+ if (urn_.size() && path.size())
+ throw std::invalid_argument("Cannot add a path (" + path + ") to an URN URI (" + urn_ + ")");
+
+ if (path[0] == '/') // if it starts with a / it is root-path
+ path_ = path;
+ else if (pos == 0) { // the URL contained only a path and the current path has no / at the end, strip last element until / and append
+ auto last_slash = path_.rfind('/');
+ path_ = path_.substr(0, last_slash) + '/' + path;
+ } else // otherwise it is a subfolder
+ path_.append(path);
+ }
+ }
+
+ pointer_ = nlohmann::json::json_pointer(pointer);
}
-const std::string json_uri::url() const
+const std::string json_uri::location() const
{
+ if (urn_.size())
+ return urn_;
+
std::stringstream s;
if (proto_.size() > 0)
@@ -115,9 +114,7 @@ std::string json_uri::to_string() const
{
std::stringstream s;
- s << urn_
- << url()
- << pointer_.to_string();
+ s << location() << " # " << pointer_.to_string();
return s.str();
}
@@ -127,48 +124,11 @@ std::ostream &operator<<(std::ostream &os, const json_uri &u)
return os << u.to_string();
}
-std::string json_uri::unescape(const std::string &src)
-{
- std::string l = src;
- std::size_t pos = src.size() - 1;
-
- do {
- pos = l.rfind('~', pos);
-
- if (pos == std::string::npos)
- break;
-
- if (pos < l.size() - 1) {
- switch (l[pos + 1]) {
- case '0':
- l.replace(pos, 2, "~");
- break;
-
- case '1':
- l.replace(pos, 2, "/");
- break;
-
- default:
- break;
- }
- }
-
- if (pos == 0)
- break;
- pos--;
- } while (pos != std::string::npos);
-
- // TODO - percent handling
-
- return l;
-}
-
std::string json_uri::escape(const std::string &src)
{
std::vector> chars = {
{"~", "~0"},
- {"/", "~1"},
- {"%", "%25"}};
+ {"/", "~1"}};
std::string l = src;
@@ -186,4 +146,4 @@ std::string json_uri::escape(const std::string &src)
return l;
}
-} // nlohmann
+} // namespace nlohmann
diff --git a/src/json-validator.cpp b/src/json-validator.cpp
index 5bfc809..e902052 100644
--- a/src/json-validator.cpp
+++ b/src/json-validator.cpp
@@ -1,782 +1,1100 @@
/*
- * Modern C++ JSON schema validator
+ * JSON schema validator for JSON for modern C++
*
- * Licensed under the MIT License .
+ * Copyright (c) 2016-2019 Patrick Boettcher
.
*
- * Copyright (c) 2016 Patrick Boettcher .
+ * SPDX-License-Identifier: MIT
*
- * 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;
+using nlohmann::json_schema::root_schema;
+using namespace nlohmann::json_schema;
#ifdef JSON_SCHEMA_BOOST_REGEX
- #include
- #define REGEX_NAMESPACE boost
+# include
+# define REGEX_NAMESPACE boost
#elif defined(JSON_SCHEMA_NO_REGEX)
- #define NO_STD_REGEX
+# define NO_STD_REGEX
#else
- #include
- #define REGEX_NAMESPACE std
+# include
+# define REGEX_NAMESPACE std
#endif
namespace
{
-class resolver
+class schema
{
- void resolve(json &schema, json_uri id)
+protected:
+ root_schema *root_;
+
+public:
+ schema(root_schema *root)
+ : root_(root) {}
+
+ virtual void validate(const json &instance, basic_error_handler &e) const = 0;
+
+ static std::shared_ptr make(json &schema,
+ root_schema *root,
+ const std::vector &key,
+ std::vector uris);
+};
+
+class logical_not : public schema
+{
+ std::shared_ptr subschema_;
+
+ void validate(const json &instance, basic_error_handler &e) const final
{
- // look for the id-field in this schema
- auto fid = schema.find("id");
+ basic_error_handler err;
+ subschema_->validate(instance, err);
- // found?
- if (fid != schema.end() &&
- fid.value().type() == json::value_t::string)
- id = id.derive(fid.value()); // resolve to a full id with URL + path based on the parent
+ if (!err)
+ e.error("", instance, "instance is valid, whereas it should NOT be as required by schema");
+ }
- // already existing - error
- if (schema_refs.find(id) != schema_refs.end())
- throw std::invalid_argument("schema " + id.to_string() + " already present in local resolver");
+public:
+ logical_not(json &sch,
+ root_schema *root,
+ const std::vector &uris)
+ : schema(root)
+ {
+ subschema_ = schema::make(sch, root, {"not"}, uris);
+ }
+};
- // store a raw pointer to this (sub-)schema referenced by its absolute json_uri
- // this (sub-)schema is part of a schema stored inside schema_store_ so we can use the a raw-pointer-ref
- schema_refs[id] = &schema;
+enum logical_combination_types {
+ allOf,
+ anyOf,
+ oneOf
+};
- for (auto i = schema.begin(), end = schema.end(); i != end; ++i) {
- // FIXME: this inhibits the user adding properties with the key "default"
- if (i.key() == "default") /* default value can be objects, but are not schemas */
- continue;
+template
+class logical_combination : public schema
+{
+ std::vector> subschemata_;
- switch (i.value().type()) {
+ void validate(const json &instance, basic_error_handler &e) const final
+ {
+ size_t count = 0;
- case json::value_t::object: // child is object, it is a schema
- resolve(i.value(), id.append(json_uri::escape(i.key())));
- break;
+ for (auto &s : subschemata_) {
+ basic_error_handler err;
+ s->validate(instance, err);
- case json::value_t::array: {
- std::size_t index = 0;
- auto child_id = id.append(json_uri::escape(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++;
+ if (err) {
+ //sub_schema_err << " one schema failed because: " << e.what() << "\n";
+ if (combine_logic == allOf) {
+ e.error("", instance, "at least one schema has failed, but ALLOF them are required to validate.");
+ return;
}
+ } else
+ count++;
+
+ if (combine_logic == oneOf && count > 1) {
+ e.error("", instance, "more than one schema has succeeded, but only ONEOF them is required to validate.");
+ return;
+ }
+ if (combine_logic == anyOf && count == 1)
+ return;
+ }
+
+ if ((combine_logic == anyOf || combine_logic == oneOf) && count == 0)
+ e.error("", instance, "no validation has succeeded but ANYOF/ONEOF them is required to validate.");
+ }
+
+public:
+ logical_combination(json &sch,
+ root_schema *root,
+ const std::vector &uris)
+ : schema(root)
+ {
+ size_t c = 0;
+ std::string key;
+ switch (combine_logic) {
+ case allOf:
+ key = "allOf";
+ break;
+ case oneOf:
+ key = "oneOf";
+ break;
+ case anyOf:
+ key = "anyOf";
+ break;
+ }
+
+ for (auto &subschema : sch)
+ subschemata_.push_back(schema::make(subschema, root, {key, std::to_string(c++)}, uris));
+ }
+};
+
+class type_schema : public schema
+{
+ std::vector> type_;
+ std::pair enum_, const_;
+ std::vector> logic_;
+
+ static std::shared_ptr make(json &schema,
+ json::value_t type,
+ root_schema *,
+ const std::vector &,
+ std::set &);
+
+ std::shared_ptr if_, then_, else_;
+
+ void validate(const json &instance, basic_error_handler &e) const override final
+ {
+ // depending on the type of instance run the type specific validator - if present
+ auto type = type_[(uint8_t) instance.type()];
+
+ if (type)
+ type->validate(instance, e);
+ else
+ e.error("", instance, "unexpected instance type");
+
+ if (enum_.first) {
+ bool seen_in_enum = false;
+ for (auto &e : enum_.second)
+ if (instance == e) {
+ seen_in_enum = true;
+ break;
+ }
+
+ if (!seen_in_enum)
+ e.error("", instance, "instance not found in required enum");
+ }
+
+ if (const_.first &&
+ const_.second != instance)
+ e.error("", instance, "instance not const");
+
+ for (auto l : logic_)
+ l->validate(instance, e);
+
+ if (if_) {
+ basic_error_handler err;
+
+ if_->validate(instance, err);
+ if (!err) {
+ if (then_)
+ then_->validate(instance, e);
+ } else {
+ if (else_)
+ else_->validate(instance, e);
+ }
+ }
+ }
+
+public:
+ type_schema(json &sch,
+ root_schema *root,
+ const std::vector &uris)
+ : schema(root), type_((uint8_t) json::value_t::discarded + 1)
+ {
+ // association between JSON-schema-type and NLohmann-types
+ static const std::vector> schema_types = {
+ {"null", json::value_t::null},
+ {"object", json::value_t::object},
+ {"array", json::value_t::array},
+ {"string", json::value_t::string},
+ {"boolean", json::value_t::boolean},
+ {"integer", json::value_t::number_integer},
+ {"integer", json::value_t::number_unsigned},
+ {"number", json::value_t::number_float},
+ };
+
+ std::set known_keywords;
+
+ auto attr = sch.find("type");
+ if (attr == sch.end()) // no type field means all sub-types possible
+ for (auto &t : schema_types)
+ type_[(uint8_t) t.second] = type_schema::make(sch, t.second, root, uris, known_keywords);
+ else {
+ switch (attr.value().type()) { // "type": "type"
+
+ case json::value_t::string: {
+ auto schema_type = attr.value().get();
+ for (auto &t : schema_types)
+ if (t.first == schema_type)
+ type_[(uint8_t) t.second] = type_schema::make(sch, t.second, root, uris, known_keywords);
} 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);
- }
+ case json::value_t::array: // "type": ["type1", "type2"]
+ for (auto &schema_type : attr.value())
+ for (auto &t : schema_types)
+ if (t.first == schema_type)
+ type_[(uint8_t) t.second] = type_schema::make(sch, t.second, root, uris, known_keywords);
break;
default:
break;
}
+
+ sch.erase(attr);
}
- }
- std::set refs;
+ for (auto &key : known_keywords)
+ sch.erase(key);
-public:
- std::set undefined_refs;
+ // with nlohmann::json floats can be seen as unsigned or integer - reuse the number-validator for
+ // integer values as well, if they have not been specified
+ if (type_[(uint8_t) json::value_t::number_float] && !type_[(uint8_t) json::value_t::number_integer])
+ type_[(uint8_t) json::value_t::number_integer] =
+ type_[(uint8_t) json::value_t::number_unsigned] =
+ type_[(uint8_t) json::value_t::number_float];
- std::map schema_refs;
+ attr = sch.find("enum");
+ if (attr != sch.end()) {
+ enum_ = {true, attr.value()};
+ sch.erase(attr);
+ }
- 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());
+ attr = sch.find("const");
+ if (attr != sch.end()) {
+ const_ = {true, attr.value()};
+ sch.erase(attr);
+ }
- resolve(schema, id);
+ attr = sch.find("not");
+ if (attr != sch.end()) {
+ logic_.push_back(std::make_shared(attr.value(), root, uris));
+ sch.erase(attr);
+ }
- // 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.url());
+ attr = sch.find("allOf");
+ if (attr != sch.end()) {
+ logic_.push_back(std::make_shared>(attr.value(), root, uris));
+ sch.erase(attr);
+ }
+
+ attr = sch.find("anyOf");
+ if (attr != sch.end()) {
+ logic_.push_back(std::make_shared>(attr.value(), root, uris));
+ sch.erase(attr);
+ }
+
+ attr = sch.find("oneOf");
+ if (attr != sch.end()) {
+ logic_.push_back(std::make_shared>(attr.value(), root, uris));
+ sch.erase(attr);
+ }
+
+ attr = sch.find("if");
+ if (attr != sch.end()) {
+ auto attr_then = sch.find("then");
+ auto attr_else = sch.find("else");
+
+ if (attr_then != sch.end() || attr_else != sch.end()) {
+ if_ = schema::make(attr.value(), root, {"if"}, uris);
+
+ if (attr_then != sch.end()) {
+ then_ = schema::make(attr_then.value(), root, {"then"}, uris);
+ sch.erase(attr_then);
+ }
+
+ if (attr_else != sch.end()) {
+ else_ = schema::make(attr_else.value(), root, {"else"}, uris);
+ sch.erase(attr_else);
+ }
}
+ sch.erase(attr);
}
}
};
-void validate_type(const json &schema, const std::string &expected_type, const std::string &name)
+class string : public schema
{
- const auto &type_it = schema.find("type");
- if (type_it == schema.end())
- /* TODO something needs to be done here, I think */
- return;
+ std::pair maxLength_{false, 0};
+ std::pair minLength_{false, 0};
- const auto &type_instance = type_it.value();
+#ifndef NO_STD_REGEX
+ std::pair pattern_{false, REGEX_NAMESPACE::regex()};
+ std::string patternString_;
+#endif
- // 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()) ||
- (expected_type == "integer" &&
- std::find(type_instance.begin(),
- type_instance.end(),
- "number") != type_instance.end()))
- return;
+ std::pair format_;
+ std::function format_check_ = nullptr;
- 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 ||
- (type_instance == "number" && expected_type == "integer"))
- return;
-
- throw std::invalid_argument(name + " is " + expected_type +
- ", but required type is " + type_instance.get());
+ std::size_t utf8_length(const std::string &s) const
+ {
+ size_t len = 0;
+ for (const unsigned char &c : s)
+ if ((c & 0xc0) != 0x80)
+ len++;
+ return len;
}
-}
-void validate_enum(const json &instance, const json &schema, const std::string &name)
-{
- const auto &enum_value = schema.find("enum");
- if (enum_value == schema.end())
- return;
+ void validate(const json &instance, basic_error_handler &e) const override
+ {
+ if (minLength_.first) {
+ if (utf8_length(instance) < minLength_.second) {
+ std::ostringstream s;
+ s << "'" << instance << "' is too short as per minLength (" << minLength_.second << ")";
+ e.error("", instance, s.str());
+ }
+ }
- if (std::find(enum_value.value().begin(), enum_value.value().end(), instance) != enum_value.value().end())
- return;
+ if (maxLength_.first) {
+ if (utf8_length(instance) > maxLength_.second) {
+ std::ostringstream s;
+ s << "'" << instance << "' is too long as per maxLength (" << maxLength_.second << ")";
+ e.error("", instance, s.str());
+ }
+ }
- std::ostringstream s;
- s << "invalid enum-value '" << instance << "' "
- << "for instance '" << name << "'. Candidates are " << enum_value.value() << ".";
+#ifndef NO_STD_REGEX
+ if (pattern_.first &&
+ !REGEX_NAMESPACE::regex_search(instance.get(), pattern_.second))
+ e.error("", instance, instance.get() + " does not match regex pattern: " + patternString_);
+#endif
- throw std::invalid_argument(s.str());
-}
-
-void validate_boolean(const json & /*instance*/, const json &schema, const std::string &name)
-{
- validate_type(schema, "boolean", name);
-}
-
-template
-bool violates_numeric_maximum(T max, T value, bool exclusive)
-{
- if (exclusive)
- return value >= max;
-
- return value > max;
-}
-
-template
-bool violates_numeric_minimum(T min, T value, bool exclusive)
-{
- if (exclusive)
- return value <= min;
-
- return value < min;
-}
-
-// multipleOf - if the rest of the division is 0 -> OK
-bool violates_multiple_of(json::number_float_t x, json::number_float_t y)
-{
- json::number_integer_t n = static_cast(x / y);
- double res = (x - n * y);
- return fabs(res) > std::numeric_limits::epsilon();
-}
-
-template
-void validate_numeric(const json &instance, const json &schema, const std::string &name)
-{
- T value = instance;
-
- if (value != 0) { // zero is multiple of everything
- const auto &multipleOf = schema.find("multipleOf");
-
- if (multipleOf != schema.end()) {
- double multiple = multipleOf.value();
- double value_float = value;
-
- if (violates_multiple_of(value_float, multiple))
- throw std::out_of_range(name + " is not a multiple of " + std::to_string(multiple));
+ if (format_.first) {
+ if (format_check_ == nullptr)
+ e.error("", instance, std::string("A format checker was not provided but a format-attribute for this string is present. ") + " cannot be validated for " + format_.second);
+ else
+ format_check_(format_.second, instance);
}
}
- const auto &maximum = schema.find("maximum");
- if (maximum != schema.end()) {
- T maxi = maximum.value();
+public:
+ string(json &sch, root_schema *root)
+ : schema(root)
+ {
+ auto attr = sch.find("maxLength");
+ if (attr != sch.end()) {
+ maxLength_ = {true, attr.value()};
+ sch.erase(attr);
+ }
- const auto &excl = schema.find("exclusiveMaximum");
- bool exclusive = (excl != schema.end()) ? excl.value().get() : false;
+ attr = sch.find("minLength");
+ if (attr != sch.end()) {
+ minLength_ = {true, attr.value()};
+ sch.erase(attr);
+ }
- if (violates_numeric_maximum(maxi, value, exclusive))
- throw std::out_of_range(name + " exceeds maximum of " + std::to_string(maxi));
+#ifndef NO_STD_REGEX
+ attr = sch.find("pattern");
+ if (attr != sch.end()) {
+ patternString_ = attr.value();
+ pattern_ = {true, REGEX_NAMESPACE::regex(attr.value().get(),
+ REGEX_NAMESPACE::regex::ECMAScript)};
+ sch.erase(attr);
+ }
+#endif
+
+ attr = sch.find("format");
+ if (attr != sch.end()) {
+ format_ = {true, attr.value()};
+ sch.erase(attr);
+ }
+ }
+};
+
+template
+class numeric : public schema
+{
+ std::pair maximum_{false, 0};
+ std::pair minimum_{false, 0};
+
+ bool exclusiveMaximum_ = false;
+ bool exclusiveMinimum_ = false;
+
+ std::pair multipleOf_{false, 0};
+
+ // multipleOf - if the rest of the division is 0 -> OK
+ bool violates_multiple_of(json::number_float_t x) const
+ {
+ json::number_integer_t n = static_cast(x / multipleOf_.second);
+ double res = (x - n * multipleOf_.second);
+ return fabs(res) > std::numeric_limits::epsilon();
}
- const auto &minimum = schema.find("minimum");
- if (minimum != schema.end()) {
- T mini = minimum.value();
+ void validate(const json &instance, basic_error_handler &e) const override
+ {
+ T value = instance; // conversion of json to value_type
- const auto &excl = schema.find("exclusiveMinimum");
- bool exclusive = (excl != schema.end()) ? excl.value().get() : false;
+ if (multipleOf_.first && value != 0) // zero is multiple of everything
+ if (violates_multiple_of(value))
+ e.error("", instance, "is not a multiple of " + std::to_string(multipleOf_.second));
- if (violates_numeric_minimum(mini, value, exclusive))
- throw std::out_of_range(name + " is below minimum of " + std::to_string(mini));
+ if (maximum_.first)
+ if ((exclusiveMaximum_ && value >= maximum_.second) ||
+ value > maximum_.second)
+ e.error("", instance, "exceeds maximum of " + std::to_string(maximum_.second));
+
+ if (minimum_.first)
+ if ((exclusiveMinimum_ && value <= minimum_.second) ||
+ value < minimum_.second)
+ e.error("", instance, "is below minimum of " + std::to_string(minimum_.second));
}
-}
-void validate_integer(const json &instance, const json &schema, const std::string &name)
+public:
+ numeric(const json &sch, root_schema *root, std::set &kw)
+ : schema(root)
+ {
+ auto attr = sch.find("maximum");
+ if (attr != sch.end()) {
+ maximum_ = {true, attr.value()};
+ kw.insert("maximum");
+ }
+
+ attr = sch.find("minimum");
+ if (attr != sch.end()) {
+ minimum_ = {true, attr.value()};
+ kw.insert("minimum");
+ }
+
+ attr = sch.find("exclusiveMaximum");
+ if (attr != sch.end()) {
+ exclusiveMaximum_ = true;
+ maximum_ = {true, attr.value()};
+ kw.insert("exclusiveMaximum");
+ }
+
+ attr = sch.find("exclusiveMinimum");
+ if (attr != sch.end()) {
+ minimum_ = {true, attr.value()};
+ exclusiveMinimum_ = true;
+ kw.insert("exclusiveMinimum");
+ }
+
+ attr = sch.find("multipleOf");
+ if (attr != sch.end()) {
+ multipleOf_ = {true, attr.value()};
+ kw.insert("multipleOf");
+ }
+ }
+};
+
+class null : public schema
{
- validate_type(schema, "integer", name);
- //TODO: Validate schema values are json::value_t::number_integer/unsigned?
+ void validate(const json &instance, basic_error_handler &e) const override
+ {
+ if (!instance.is_null())
+ e.error("", instance, "expected to be null");
+ }
- validate_numeric(instance, schema, name);
-}
+public:
+ null(json &, root_schema *root)
+ : schema(root) {}
+};
-bool is_unsigned(const json &schema)
+class boolean_type : public schema
{
- const auto &minimum = schema.find("minimum");
+ void validate(const json &, basic_error_handler &) const override {}
- // Number is expected to be unsigned if a minimum >= 0 is set
- return minimum != schema.end() && minimum.value() >= 0;
-}
+public:
+ boolean_type(json &, root_schema *root)
+ : schema(root) {}
+};
-void validate_unsigned(const json &instance, const json &schema, const std::string &name)
+class boolean : public schema
{
- validate_type(schema, "integer", name);
- //TODO: Validate schema values are json::value_t::unsigned?
+ bool true_;
+ void validate(const json &instance, basic_error_handler &e) const override
+ {
+ if (!true_) { // false schema
+ // empty array
+ //switch (instance.type()) {
+ //case json::value_t::array:
+ // if (instance.size() != 0) // valid false-schema
+ // e.error("", instance, "false-schema required empty array");
+ // return;
+ //}
- //Is there a better way to determine whether an unsigned comparison should take place?
- if (is_unsigned(schema))
- validate_numeric(instance, schema, name);
- else
- validate_numeric(instance, schema, name);
-}
+ e.error("", instance, "instance invalid as par false-schema");
+ }
+ }
-void validate_float(const json &instance, const json &schema, const std::string &name)
+public:
+ boolean(json &sch, root_schema *root)
+ : schema(root), true_(sch) {}
+};
+
+class required : public schema
{
- validate_type(schema, "number", name);
- //TODO: Validate schema values are json::value_t::number_float?
+ const std::vector required_;
- validate_numeric(instance, schema, name);
-}
+ void validate(const json &instance, basic_error_handler &e) const override final
+ {
+ for (auto &r : required_)
+ if (instance.find(r) == instance.end())
+ e.error("", instance, "required property '" + r + "' not found in object as a dependency");
+ }
-void validate_null(const json & /*instance*/, const json &schema, const std::string &name)
+public:
+ required(const std::vector &r, root_schema *root)
+ : schema(root), required_(r) {}
+};
+
+class object : public schema
{
- validate_type(schema, "null", name);
-}
+ std::pair maxProperties_{false, 0};
+ std::pair minProperties_{false, 0};
+ std::vector required_;
-} // anonymous namespace
+ std::map> properties_;
+#ifndef NO_STD_REGEX
+ std::vector>> patternProperties_;
+#endif
+ std::shared_ptr additionalProperties_;
+
+ std::map> dependencies_;
+
+ std::shared_ptr propertyNames_;
+
+ void validate(const json &instance, basic_error_handler &e) const override
+ {
+ if (maxProperties_.first && instance.size() > maxProperties_.second)
+ e.error("", instance, "too many properties.");
+
+ if (minProperties_.first && instance.size() < minProperties_.second)
+ e.error("", instance, "too few properties.");
+
+ for (auto &r : required_)
+ if (instance.find(r) == instance.end())
+ e.error("", instance, "required property '" + r + "' not found in object '");
+
+ // for each property in instance
+ for (auto &p : instance.items()) {
+ if (propertyNames_)
+ propertyNames_->validate(p.key(), e);
+
+ bool a_prop_or_pattern_matched = false;
+ auto schema_p = properties_.find(p.key());
+ // check if it is in "properties"
+ if (schema_p != properties_.end()) {
+ a_prop_or_pattern_matched = true;
+ schema_p->second->validate(p.value(), e);
+ }
+
+ // check all matching patternProperties
+ for (auto &schema_pp : patternProperties_)
+ if (REGEX_NAMESPACE::regex_search(p.key(), schema_pp.first)) {
+ a_prop_or_pattern_matched = true;
+ schema_pp.second->validate(p.value(), e);
+ }
+ // check additionalProperties as a last resort
+ if (!a_prop_or_pattern_matched && additionalProperties_)
+ additionalProperties_->validate(p.value(), e);
+ }
+
+ for (auto &dep : dependencies_) {
+ auto prop = instance.find(dep.first);
+ if (prop != instance.end()) // if dependency-property is present in instance
+ dep.second->validate(instance, e); // validate
+ }
+ }
+
+public:
+ object(json &sch,
+ root_schema *root,
+ const std::vector &uris)
+ : schema(root)
+ {
+ auto attr = sch.find("maxProperties");
+ if (attr != sch.end()) {
+ maxProperties_ = {true, attr.value()};
+ sch.erase(attr);
+ }
+
+ attr = sch.find("minProperties");
+ if (attr != sch.end()) {
+ minProperties_ = {true, attr.value()};
+ sch.erase(attr);
+ }
+
+ attr = sch.find("required");
+ if (attr != sch.end()) {
+ required_ = attr.value().get>();
+ sch.erase(attr);
+ }
+
+ attr = sch.find("properties");
+ if (attr != sch.end()) {
+ for (auto prop : attr.value().items())
+ properties_.insert(
+ std::make_pair(
+ prop.key(),
+ schema::make(prop.value(), root, {"properties", prop.key()}, uris)));
+ sch.erase(attr);
+ }
+
+#ifndef NO_STD_REGEX
+ attr = sch.find("patternProperties");
+ if (attr != sch.end()) {
+ for (auto prop : attr.value().items())
+ patternProperties_.push_back(
+ std::make_pair(
+ REGEX_NAMESPACE::regex(prop.key(), REGEX_NAMESPACE::regex::ECMAScript),
+ schema::make(prop.value(), root, {prop.key()}, uris)));
+ sch.erase(attr);
+ }
+#endif
+
+ attr = sch.find("additionalProperties");
+ if (attr != sch.end()) {
+ additionalProperties_ = schema::make(attr.value(), root, {"additionalProperties"}, uris);
+ sch.erase(attr);
+ }
+
+ attr = sch.find("dependencies");
+ if (attr != sch.end()) {
+ for (auto &dep : attr.value().items())
+ switch (dep.value().type()) {
+ case json::value_t::array:
+ dependencies_.emplace(dep.key(),
+ std::make_shared(
+ dep.value().get>(), root));
+ break;
+
+ default:
+ dependencies_.emplace(dep.key(),
+ schema::make(dep.value(), root, {"dependencies", dep.key()}, uris));
+ break;
+ }
+ sch.erase(attr);
+ }
+
+ attr = sch.find("propertyNames");
+ if (attr != sch.end()) {
+ propertyNames_ = schema::make(attr.value(), root, {"propertyNames"}, uris);
+ sch.erase(attr);
+ }
+ }
+};
+
+class array : public schema
+{
+ std::pair maxItems_{false, 0};
+ std::pair minItems_{false, 0};
+ bool uniqueItems_ = false;
+
+ std::shared_ptr items_schema_;
+
+ std::vector> items_;
+ std::shared_ptr additionalItems_;
+
+ std::shared_ptr contains_;
+
+ void validate(const json &instance, basic_error_handler &e) const override
+ {
+ if (maxItems_.first && instance.size() > maxItems_.second)
+ e.error("", instance, "has too many items.");
+
+ if (minItems_.first && instance.size() < minItems_.second)
+ e.error("", instance, "has too few items.");
+
+ if (uniqueItems_) {
+ for (auto it = instance.cbegin(); it != instance.cend(); ++it) {
+ auto v = std::find(it + 1, instance.end(), *it);
+ if (v != instance.end())
+ e.error("", instance, "items have to be unique for this array.");
+ }
+ }
+
+ if (items_schema_)
+ for (auto &i : instance)
+ items_schema_->validate(i, e);
+ else {
+ auto item = items_.cbegin();
+ for (auto &i : instance) {
+ std::shared_ptr item_validator;
+ if (item == items_.cend())
+ item_validator = additionalItems_;
+ else {
+ item_validator = *item;
+ item++;
+ }
+
+ if (!item_validator)
+ break;
+
+ item_validator->validate(i, e);
+ }
+ }
+
+ if (contains_) {
+ bool contained = false;
+ for (auto &item : instance) {
+ basic_error_handler local_e;
+ contains_->validate(item, local_e);
+ if (!local_e) {
+ contained = true;
+ break;
+ }
+ }
+ if (!contained)
+ e.error("", instance, "array does not contain required element as per 'contains'");
+ }
+ }
+
+public:
+ array(json &sch, root_schema *root, const std::vector &uris)
+ : schema(root)
+ {
+ auto attr = sch.find("maxItems");
+ if (attr != sch.end()) {
+ maxItems_ = {true, attr.value()};
+ sch.erase(attr);
+ }
+
+ attr = sch.find("minItems");
+ if (attr != sch.end()) {
+ minItems_ = {true, attr.value()};
+ sch.erase(attr);
+ }
+
+ attr = sch.find("uniqueItems");
+ if (attr != sch.end()) {
+ uniqueItems_ = attr.value();
+ sch.erase(attr);
+ }
+
+ attr = sch.find("items");
+ if (attr != sch.end()) {
+
+ if (attr.value().type() == json::value_t::array) {
+ size_t c = 0;
+ for (auto &subsch : attr.value())
+ items_.push_back(schema::make(subsch, root, {"items", std::to_string(c++)}, uris));
+
+ auto attr_add = sch.find("additionalItems");
+ if (attr_add != sch.end()) {
+ additionalItems_ = schema::make(attr_add.value(), root, {"additionalItems"}, uris);
+ sch.erase(attr_add);
+ }
+
+ } else if (attr.value().type() == json::value_t::object ||
+ attr.value().type() == json::value_t::boolean)
+ items_schema_ = schema::make(attr.value(), root, {"items"}, uris);
+
+ sch.erase(attr);
+ }
+
+ attr = sch.find("contains");
+ if (attr != sch.end()) {
+ contains_ = schema::make(attr.value(), root, {"contains"}, uris);
+ sch.erase(attr);
+ }
+ }
+};
+
+class schema_ref : public schema
+{
+ const std::string id_;
+ std::shared_ptr target_;
+
+ void validate(const json &instance, basic_error_handler &e) const final
+ {
+ if (target_)
+ target_->validate(instance, e);
+ else
+ e.error("", instance, "unresolved schema-reference " + id_);
+ }
+
+public:
+ schema_ref(const std::string &id, root_schema *root)
+ : schema(root), id_(id) {}
+
+ const std::string &id() const { return id_; }
+ void set_target(std::shared_ptr target) { target_ = target; }
+};
+
+std::shared_ptr type_schema::make(json &schema,
+ json::value_t type,
+ root_schema *root,
+ const std::vector &uris,
+ std::set &kw)
+{
+ switch (type) {
+ case json::value_t::null:
+ return std::make_shared(schema, root);
+ case json::value_t::number_unsigned:
+ return std::make_shared>(schema, root, kw);
+ case json::value_t::number_integer:
+ return std::make_shared>(schema, root, kw);
+ case json::value_t::number_float:
+ return std::make_shared>(schema, root, kw);
+ case json::value_t::string:
+ return std::make_shared(schema, root);
+ case json::value_t::boolean:
+ return std::make_shared(schema, root);
+ case json::value_t::object:
+ return std::make_shared