initial commit

This commit is contained in:
Patrick Boettcher 2016-12-22 18:47:31 +01:00
commit bf4cef21c3
9 changed files with 657 additions and 0 deletions

15
.clang-format Normal file
View File

@ -0,0 +1,15 @@
---
BasedOnStyle: LLVM
AccessModifierOffset: -2
#AlignConsecutiveAssignments: true
#AlignConsecutiveDeclarations: true
AllowShortFunctionsOnASingleLine: Inline
BreakBeforeBraces: Linux
ColumnLimit: 0
ConstructorInitializerAllOnOneLineOrOnePerLine: true
IndentWidth: 2
ObjCBlockIndentWidth: 2
SpaceAfterCStyleCast: true
TabWidth: 2
UseTab: ForIndentation
...

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
build*/
*.sw?

63
CMakeLists.txt Normal file
View File

@ -0,0 +1,63 @@
project(json-schema-validator CXX)
cmake_minimum_required(VERSION 3.2)
# find nlohmann's json.hpp
find_path(NLOHMANN_JSON_DIR
NAMES
json.hpp)
if(NOT NLOHMANN_JSON_DIR)
message(FATAL_ERROR "please set NLOHMANN_JSON_DIR to a path in which NLohmann's json.hpp can be found.")
endif()
# create an interface-library for simple linking
add_library(json INTERFACE)
target_include_directories(json
INTERFACE
${NLOHMANN_JSON_DIR})
# and one for the validator
add_library(json-schema-validator INTERFACE)
target_include_directories(json-schema-validator
INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/src)
target_compile_options(json-schema-validator
INTERFACE
-Wall -Wextra) # bad, better use something else based on compiler type
target_link_libraries(json-schema-validator
INTERFACE
json)
# simple json-schema-validator-executable
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)
enable_testing()
# find schema-test-suite
find_path(JSON_SCHEMA_TEST_SUITE_PATH
NAMES
tests/draft4)
if(JSON_SCHEMA_TEST_SUITE_PATH)
# create tests foreach test-file
file(GLOB_RECURSE TEST_FILES ${JSON_SCHEMA_TEST_SUITE_PATH}/tests/draft4/*.json)
foreach(TEST_FILE ${TEST_FILES})
get_filename_component(TEST_NAME ${TEST_FILE} NAME_WE)
add_test(
NAME ${TEST_NAME}
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/test.sh $<TARGET_FILE:json-schema-test> ${TEST_FILE}
)
endforeach()
else()
message(STATUS "Please test JSON_SCHEMA_TEST_SUITE_PATH to a path in which you've cloned JSON-Schema-Test-Suite (github.com/json-schema-org/JSON-Schema-Test-Suite).")
endif()

22
LICENSE.mit Normal file
View File

@ -0,0 +1,22 @@
Modern C++ JSON schema validator is licensed under the MIT License
<http://opensource.org/licenses/MIT>:
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.

82
README.md Normal file
View File

@ -0,0 +1,82 @@
# Modern C++ JSON schema validator
# What is it?
This is a C++ header-only 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).
First a disclaimer: *Everything here should be considered work in progress and
contributions or hints or discussions are welcome.*
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 much
modern C++ inside.
External documentation is missing as well. However the API of the validator
will be rather simple.
# How to use
## Build
```Bash
git clone https://github.com/pboettch/json-schema-validator.git
cd json-schema-validator
mkdir build
cd build
cmake .. \
-DNLOHMANN_JSON_DIR=<path/to/json.hpp> \
-DJSON_SCHEMA_TEST_SUITE_PATH=<path/to/JSON-Schema-test-suite> # optional
make
```
## Code
See also `app/json-schema-validate.cpp`.
```C++
#include "json-schema-validator.hpp"
using nlohmann::json;
using nlohmann::json_validator;
int main(void)
{
json schema, document;
/* fill in the schema */
/* fill in the document */
json_validator validator;
try {
validator.validate(document, scheam);
} catch (const std::out_of_range &e) {
std::cerr << "Validation failed, here is why: " << e.what() << "\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
```
# Conformity
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 more 150 tests are still failing, because simply not all keyword and
their functionalities have been implemented. Some of the missing feature will
require a rework.
# Additional features
## Default value population
For my use case I need something to populate default values into the JSON
instance of properties which are not set by the user.
This feature can be enable by setting the `default_value_insertion` to true.

61
app/json-schema-test.cpp Normal file
View File

@ -0,0 +1,61 @@
#include "json-schema-validator.hpp"
using nlohmann::json;
using nlohmann::json_validator;
int main(void)
{
json validation;
try {
std::cin >> validation;
} catch (std::exception &e) {
std::cerr << e.what() << "\n";
return EXIT_FAILURE;
}
json_validator validator;
size_t failed = 0,
total = 0;
for (auto &test_group : validation) {
std::cerr << "Testing Group " << test_group["description"] << "\n";
const auto &schema = test_group["schema"];
for (auto &test_case : test_group["tests"]) {
std::cerr << " Testing Case " << test_case["description"] << "\n";
bool valid = true;
try {
validator.validate(test_case["data"], schema);
} catch (const std::out_of_range &e) {
valid = false;
std::cerr << " Test Case Exception (out of range): " << e.what() << "\n";
} catch (const std::invalid_argument &e) {
valid = false;
std::cerr << " Test Case Exception (invalid argument): " << e.what() << "\n";
} catch (const std::logic_error &e) {
valid = !test_case["valid"]; /* force test-case failure */
std::cerr << " Not yet implemented: " << e.what() << "\n";
}
if (valid == test_case["valid"])
std::cerr << " --> Test Case exited with " << valid << " as expected.\n";
else {
failed++;
std::cerr << " --> Test Case exited with " << valid << " NOT expected.\n";
}
total++;
std::cerr << "\n";
}
std::cerr << "-------------\n";
}
std::cerr << (total - failed) << " of " << total << " have succeeded - " << failed << " failed\n";
return failed;
}

View File

@ -0,0 +1,58 @@
#include "json-schema-validator.hpp"
#include <fstream>
#include <cstdlib>
using nlohmann::json;
using nlohmann::json_validator;
static void usage(const char *name)
{
std::cerr << "Usage: " << name << " <json-document> < <schema>\n";
exit(EXIT_FAILURE);
}
int main(int argc, char *argv[])
{
if (argc != 2)
usage(argv[0]);
std::fstream f(argv[1]);
if (!f.good()) {
std::cerr << "could not open " << argv[1] << " for reading\n";
usage(argv[0]);
}
json schema;
try {
f >> schema;
} catch (std::exception &e) {
std::cerr << e.what() << " at " << f.tellp() << "\n";
return EXIT_FAILURE;
}
json document;
try {
std::cin >> document;
} catch (std::exception &e) {
std::cerr << e.what() << " at " << f.tellp() << "\n";
return EXIT_FAILURE;
}
json_validator validator;
try {
validator.validate(document, schema);
} catch (std::exception &e) {
std::cerr << "schema validation failed\n";
std::cerr << e.what() << "\n";
return EXIT_FAILURE;
}
std::cerr << std::setw(2) << document << "\n";
return EXIT_SUCCESS;
}

View File

@ -0,0 +1,340 @@
#ifndef NLOHMANN_JSON_VALIDATOR_HPP__
#define NLOHMANN_JSON_VALIDATOR_HPP__
#include <json.hpp>
#include <regex>
// make yourself a home - welcome to nlohmann's namespace
namespace nlohmann
{
class json_validator
{
// insert default values items into object
// if the key is not present before checking their
// validity in regards to their schema
//
// breaks JSON-Schema-Test-Suite if true
// *PARTIALLY IMPLEMENTED* only for properties of objects
bool default_value_insertion = false;
// recursively insert default values and create parent objects if
// they would be empty
//
// breaks JSON-Schema-Test-Suite if true
// *NOT YET IMPLEMENTED* -> maybe the same as the above option, need more thoughts
bool recursive_default_value_insertion = false;
void not_yet_implemented(const json &schema, const std::string &field, const std::string &type)
{
if (schema.find(field) != schema.end())
throw std::logic_error(field + " for " + type + " is not yet implemented");
}
void validate_type(const json &schema, const std::string &expected_type, const std::string &name)
{
const auto &type_it = schema.find("type");
if (type_it == schema.end())
/* TODO guess type for more safety,
* TODO use definitions
* TODO valid by not being defined? FIXME not clear - there are
* schema-test case which are not specifying a type */
return;
const auto &type_instance = type_it.value();
// any of the types in this array
if (type_instance.type() == json::value_t::array) {
if (std::find(type_instance.begin(),
type_instance.end(),
expected_type) != type_instance.end())
return;
std::ostringstream s;
s << expected_type << " is not any of " << type_instance << " for " << name;
throw std::invalid_argument(s.str());
} else { // type_instance is a string
if (type_instance == expected_type)
return;
throw std::invalid_argument(type_instance.get<std::string>() + " is not a " + expected_type + " for " + name);
}
}
void validate_enum(json &instance, const json &schema, const std::string &name)
{
const auto &enum_value = schema.find("enum");
if (enum_value == schema.end())
return;
if (std::find(enum_value.value().begin(), enum_value.value().end(), instance) != enum_value.value().end())
return;
std::ostringstream s;
s << "invalid enum-value '" << instance << "' "
<< "for instance '" << name << "'. Candidates are " << enum_value.value() << ".";
throw std::invalid_argument(s.str());
}
void validate_string(json &instance, const json &schema, const std::string &name)
{
// possibile but unhanled keywords
not_yet_implemented(schema, "format", "string");
not_yet_implemented(schema, "pattern", "string");
validate_type(schema, "string", name);
auto attr = schema.find("minLength");
if (attr != schema.end())
if (instance.get<std::string>().size() < attr.value()) {
std::ostringstream s;
s << "'" << name << "' of value '" << instance << "' is too short as per minLength ("
<< attr.value() << ")";
throw std::out_of_range(s.str());
}
attr = schema.find("maxLength");
if (attr != schema.end())
if (instance.get<std::string>().size() > attr.value()) {
std::ostringstream s;
s << "'" << name << "' of value '" << instance << "' is too long as per maxLength ("
<< attr.value() << ")";
throw std::out_of_range(s.str());
}
}
void validate_boolean(json & /*instance*/, const json &schema, const std::string &name)
{
validate_type(schema, "boolean", name);
}
void validate_numeric(json &instance, const json &schema, const std::string &name)
{
double value = instance;
const auto &multipleOf = schema.find("multipleOf");
if (multipleOf != schema.end()) {
double rem = fmod(value, multipleOf.value());
if (rem != 0.0)
throw std::out_of_range(name + " is not a multiple ...");
}
const auto &maximum = schema.find("maximum");
if (maximum != schema.end()) {
double maxi = maximum.value();
auto ex = std::out_of_range(name + " exceeds maximum of ...");
if (schema.find("exclusiveMaximum") != schema.end()) {
if (value >= maxi)
throw ex;
} else {
if (value > maxi)
throw ex;
}
}
const auto &minimum = schema.find("minimum");
if (minimum != schema.end()) {
double mini = minimum.value();
auto ex = std::out_of_range(name + " exceeds minimum of ...");
if (schema.find("exclusiveMinimum") != schema.end()) {
if (value <= mini)
throw ex;
} else {
if (value < mini)
throw ex;
}
}
}
void validate_integer(json &instance, const json &schema, const std::string &name)
{
validate_type(schema, "integer", name);
validate_numeric(instance, schema, name);
}
void validate_unsigned(json &instance, const json &schema, const std::string &name)
{
validate_type(schema, "integer", name);
validate_numeric(instance, schema, name);
}
void validate_float(json &instance, const json &schema, const std::string &name)
{
validate_type(schema, "number", name);
validate_numeric(instance, schema, name);
}
void validate_null(json & /*instance*/, const json &schema, const std::string &name)
{
validate_type(schema, "null", name);
}
void validate_array(json & /*instance*/, const json &schema, const std::string &name)
{
not_yet_implemented(schema, "maxItems", "array");
not_yet_implemented(schema, "minItems", "array");
not_yet_implemented(schema, "uniqueItems", "array");
not_yet_implemented(schema, "items", "array");
not_yet_implemented(schema, "additionalItems", "array");
validate_type(schema, "array", name);
}
void validate_object(json &instance, const json &schema, const std::string &name)
{
not_yet_implemented(schema, "maxProperties", "object");
not_yet_implemented(schema, "minProperties", "object");
not_yet_implemented(schema, "dependencies", "object");
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();
}
// 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;
}
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;
};
}
// check for required elements which are not present
const auto &required = schema.find("required");
if (required == schema.end())
return;
for (const auto &element : required.value()) {
if (instance.find(element) == instance.end()) {
throw std::invalid_argument("required element '" + element.get<std::string>() +
"' not found in object '" + name + "'");
}
}
}
public:
void validate(json &instance, const json &schema, const std::string &name = "root")
{
not_yet_implemented(schema, "allOf", "all");
not_yet_implemented(schema, "anyOf", "all");
not_yet_implemented(schema, "oneOf", "all");
not_yet_implemented(schema, "not", "all");
not_yet_implemented(schema, "definitions", "all");
not_yet_implemented(schema, "$ref", "all");
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:
throw std::out_of_range("type '" + schema["type"].get<std::string>() +
"' has no validator yet");
break;
}
}
};
}
#endif /* NLOHMANN_JSON_VALIDATOR_HPP__ */

13
test.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/sh
if [ ! -x "$1" ]
then
exit 1
fi
if [ ! -e "$2" ]
then
exit 1
fi
$1 < $2