Skip to content

Commit bf4cef2

Browse files
committedDec 22, 2016
initial commit
0 parents  commit bf4cef2

9 files changed

+657
-0
lines changed
 

‎.clang-format

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
BasedOnStyle: LLVM
3+
AccessModifierOffset: -2
4+
#AlignConsecutiveAssignments: true
5+
#AlignConsecutiveDeclarations: true
6+
AllowShortFunctionsOnASingleLine: Inline
7+
BreakBeforeBraces: Linux
8+
ColumnLimit: 0
9+
ConstructorInitializerAllOnOneLineOrOnePerLine: true
10+
IndentWidth: 2
11+
ObjCBlockIndentWidth: 2
12+
SpaceAfterCStyleCast: true
13+
TabWidth: 2
14+
UseTab: ForIndentation
15+
...

‎.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
build*/
2+
*.sw?
3+

‎CMakeLists.txt

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
project(json-schema-validator CXX)
2+
3+
cmake_minimum_required(VERSION 3.2)
4+
5+
# find nlohmann's json.hpp
6+
find_path(NLOHMANN_JSON_DIR
7+
NAMES
8+
json.hpp)
9+
10+
if(NOT NLOHMANN_JSON_DIR)
11+
message(FATAL_ERROR "please set NLOHMANN_JSON_DIR to a path in which NLohmann's json.hpp can be found.")
12+
endif()
13+
14+
# create an interface-library for simple linking
15+
add_library(json INTERFACE)
16+
target_include_directories(json
17+
INTERFACE
18+
${NLOHMANN_JSON_DIR})
19+
20+
# and one for the validator
21+
add_library(json-schema-validator INTERFACE)
22+
target_include_directories(json-schema-validator
23+
INTERFACE
24+
${CMAKE_CURRENT_SOURCE_DIR}/src)
25+
target_compile_options(json-schema-validator
26+
INTERFACE
27+
-Wall -Wextra) # bad, better use something else based on compiler type
28+
target_link_libraries(json-schema-validator
29+
INTERFACE
30+
json)
31+
32+
# simple json-schema-validator-executable
33+
add_executable(json-schema-validate app/json-schema-validate.cpp)
34+
target_link_libraries(json-schema-validate json-schema-validator)
35+
36+
# json-schema-validator-tester
37+
add_executable(json-schema-test app/json-schema-test.cpp)
38+
target_link_libraries(json-schema-test json-schema-validator)
39+
40+
enable_testing()
41+
42+
# find schema-test-suite
43+
find_path(JSON_SCHEMA_TEST_SUITE_PATH
44+
NAMES
45+
tests/draft4)
46+
47+
if(JSON_SCHEMA_TEST_SUITE_PATH)
48+
# create tests foreach test-file
49+
file(GLOB_RECURSE TEST_FILES ${JSON_SCHEMA_TEST_SUITE_PATH}/tests/draft4/*.json)
50+
51+
foreach(TEST_FILE ${TEST_FILES})
52+
get_filename_component(TEST_NAME ${TEST_FILE} NAME_WE)
53+
add_test(
54+
NAME ${TEST_NAME}
55+
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/test.sh $<TARGET_FILE:json-schema-test> ${TEST_FILE}
56+
)
57+
endforeach()
58+
else()
59+
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).")
60+
endif()
61+
62+
63+

‎LICENSE.mit

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Modern C++ JSON schema validator is licensed under the MIT License
2+
<http://opensource.org/licenses/MIT>:
3+
4+
Copyright (c) 2016 Patrick Boettcher
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy of
7+
this software and associated documentation files (the "Software"), to deal in
8+
the Software without restriction, including without limitation the rights to
9+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10+
of the Software, and to permit persons to whom the Software is furnished to do
11+
so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.

‎README.md

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Modern C++ JSON schema validator
2+
3+
# What is it?
4+
5+
This is a C++ header-only library for validating JSON documents based on a
6+
[JSON Schema](http://json-schema.org/) which itself should validate with
7+
[draft-4 of JSON Schema Validation](http://json-schema.org/schema).
8+
9+
First a disclaimer: *Everything here should be considered work in progress and
10+
contributions or hints or discussions are welcome.*
11+
12+
Niels Lohmann et al develop a great JSON parser for C++ called [JSON for Modern
13+
C++](https://github.com/nlohmann/json). This validator is based on this
14+
library, hence the name.
15+
16+
The name is for the moment purely marketing, because there is, IMHO, not much
17+
modern C++ inside.
18+
19+
External documentation is missing as well. However the API of the validator
20+
will be rather simple.
21+
22+
# How to use
23+
24+
## Build
25+
26+
```Bash
27+
git clone https://github.com/pboettch/json-schema-validator.git
28+
cd json-schema-validator
29+
mkdir build
30+
cd build
31+
cmake .. \
32+
-DNLOHMANN_JSON_DIR=<path/to/json.hpp> \
33+
-DJSON_SCHEMA_TEST_SUITE_PATH=<path/to/JSON-Schema-test-suite> # optional
34+
make
35+
```
36+
37+
## Code
38+
39+
See also `app/json-schema-validate.cpp`.
40+
41+
```C++
42+
#include "json-schema-validator.hpp"
43+
44+
using nlohmann::json;
45+
using nlohmann::json_validator;
46+
47+
int main(void)
48+
{
49+
json schema, document;
50+
51+
/* fill in the schema */
52+
/* fill in the document */
53+
54+
json_validator validator;
55+
56+
try {
57+
validator.validate(document, scheam);
58+
} catch (const std::out_of_range &e) {
59+
std::cerr << "Validation failed, here is why: " << e.what() << "\n";
60+
return EXIT_FAILURE;
61+
}
62+
return EXIT_SUCCESS;
63+
}
64+
```
65+
66+
# Conformity
67+
68+
There is an application which can be used for testing the validator with the
69+
[JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite).
70+
71+
Currently more 150 tests are still failing, because simply not all keyword and
72+
their functionalities have been implemented. Some of the missing feature will
73+
require a rework.
74+
75+
# Additional features
76+
77+
## Default value population
78+
79+
For my use case I need something to populate default values into the JSON
80+
instance of properties which are not set by the user.
81+
82+
This feature can be enable by setting the `default_value_insertion` to true.

‎app/json-schema-test.cpp

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#include "json-schema-validator.hpp"
2+
3+
using nlohmann::json;
4+
using nlohmann::json_validator;
5+
6+
int main(void)
7+
{
8+
json validation;
9+
10+
try {
11+
std::cin >> validation;
12+
} catch (std::exception &e) {
13+
std::cerr << e.what() << "\n";
14+
return EXIT_FAILURE;
15+
}
16+
17+
json_validator validator;
18+
19+
size_t failed = 0,
20+
total = 0;
21+
22+
for (auto &test_group : validation) {
23+
24+
std::cerr << "Testing Group " << test_group["description"] << "\n";
25+
26+
const auto &schema = test_group["schema"];
27+
28+
for (auto &test_case : test_group["tests"]) {
29+
std::cerr << " Testing Case " << test_case["description"] << "\n";
30+
31+
bool valid = true;
32+
33+
try {
34+
validator.validate(test_case["data"], schema);
35+
} catch (const std::out_of_range &e) {
36+
valid = false;
37+
std::cerr << " Test Case Exception (out of range): " << e.what() << "\n";
38+
} catch (const std::invalid_argument &e) {
39+
valid = false;
40+
std::cerr << " Test Case Exception (invalid argument): " << e.what() << "\n";
41+
} catch (const std::logic_error &e) {
42+
valid = !test_case["valid"]; /* force test-case failure */
43+
std::cerr << " Not yet implemented: " << e.what() << "\n";
44+
}
45+
46+
if (valid == test_case["valid"])
47+
std::cerr << " --> Test Case exited with " << valid << " as expected.\n";
48+
else {
49+
failed++;
50+
std::cerr << " --> Test Case exited with " << valid << " NOT expected.\n";
51+
}
52+
total++;
53+
std::cerr << "\n";
54+
}
55+
std::cerr << "-------------\n";
56+
}
57+
58+
std::cerr << (total - failed) << " of " << total << " have succeeded - " << failed << " failed\n";
59+
60+
return failed;
61+
}

‎app/json-schema-validate.cpp

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#include "json-schema-validator.hpp"
2+
3+
#include <fstream>
4+
5+
#include <cstdlib>
6+
7+
using nlohmann::json;
8+
using nlohmann::json_validator;
9+
10+
static void usage(const char *name)
11+
{
12+
std::cerr << "Usage: " << name << " <json-document> < <schema>\n";
13+
exit(EXIT_FAILURE);
14+
}
15+
16+
int main(int argc, char *argv[])
17+
{
18+
if (argc != 2)
19+
usage(argv[0]);
20+
21+
std::fstream f(argv[1]);
22+
if (!f.good()) {
23+
std::cerr << "could not open " << argv[1] << " for reading\n";
24+
usage(argv[0]);
25+
}
26+
27+
json schema;
28+
29+
try {
30+
f >> schema;
31+
} catch (std::exception &e) {
32+
std::cerr << e.what() << " at " << f.tellp() << "\n";
33+
return EXIT_FAILURE;
34+
}
35+
36+
json document;
37+
38+
try {
39+
std::cin >> document;
40+
} catch (std::exception &e) {
41+
std::cerr << e.what() << " at " << f.tellp() << "\n";
42+
return EXIT_FAILURE;
43+
}
44+
45+
json_validator validator;
46+
47+
try {
48+
validator.validate(document, schema);
49+
} catch (std::exception &e) {
50+
std::cerr << "schema validation failed\n";
51+
std::cerr << e.what() << "\n";
52+
return EXIT_FAILURE;
53+
}
54+
55+
std::cerr << std::setw(2) << document << "\n";
56+
57+
return EXIT_SUCCESS;
58+
}

‎src/json-schema-validator.hpp

+340
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
#ifndef NLOHMANN_JSON_VALIDATOR_HPP__
2+
#define NLOHMANN_JSON_VALIDATOR_HPP__
3+
4+
#include <json.hpp>
5+
6+
#include <regex>
7+
8+
// make yourself a home - welcome to nlohmann's namespace
9+
namespace nlohmann
10+
{
11+
12+
class json_validator
13+
{
14+
// insert default values items into object
15+
// if the key is not present before checking their
16+
// validity in regards to their schema
17+
//
18+
// breaks JSON-Schema-Test-Suite if true
19+
// *PARTIALLY IMPLEMENTED* only for properties of objects
20+
bool default_value_insertion = false;
21+
22+
// recursively insert default values and create parent objects if
23+
// they would be empty
24+
//
25+
// breaks JSON-Schema-Test-Suite if true
26+
// *NOT YET IMPLEMENTED* -> maybe the same as the above option, need more thoughts
27+
bool recursive_default_value_insertion = false;
28+
29+
void not_yet_implemented(const json &schema, const std::string &field, const std::string &type)
30+
{
31+
if (schema.find(field) != schema.end())
32+
throw std::logic_error(field + " for " + type + " is not yet implemented");
33+
}
34+
35+
void validate_type(const json &schema, const std::string &expected_type, const std::string &name)
36+
{
37+
const auto &type_it = schema.find("type");
38+
if (type_it == schema.end())
39+
/* TODO guess type for more safety,
40+
* TODO use definitions
41+
* TODO valid by not being defined? FIXME not clear - there are
42+
* schema-test case which are not specifying a type */
43+
return;
44+
45+
const auto &type_instance = type_it.value();
46+
47+
// any of the types in this array
48+
if (type_instance.type() == json::value_t::array) {
49+
if (std::find(type_instance.begin(),
50+
type_instance.end(),
51+
expected_type) != type_instance.end())
52+
return;
53+
54+
std::ostringstream s;
55+
s << expected_type << " is not any of " << type_instance << " for " << name;
56+
throw std::invalid_argument(s.str());
57+
58+
} else { // type_instance is a string
59+
if (type_instance == expected_type)
60+
return;
61+
62+
throw std::invalid_argument(type_instance.get<std::string>() + " is not a " + expected_type + " for " + name);
63+
}
64+
}
65+
66+
void validate_enum(json &instance, const json &schema, const std::string &name)
67+
{
68+
const auto &enum_value = schema.find("enum");
69+
if (enum_value == schema.end())
70+
return;
71+
72+
if (std::find(enum_value.value().begin(), enum_value.value().end(), instance) != enum_value.value().end())
73+
return;
74+
75+
std::ostringstream s;
76+
s << "invalid enum-value '" << instance << "' "
77+
<< "for instance '" << name << "'. Candidates are " << enum_value.value() << ".";
78+
79+
throw std::invalid_argument(s.str());
80+
}
81+
82+
void validate_string(json &instance, const json &schema, const std::string &name)
83+
{
84+
// possibile but unhanled keywords
85+
not_yet_implemented(schema, "format", "string");
86+
not_yet_implemented(schema, "pattern", "string");
87+
88+
validate_type(schema, "string", name);
89+
90+
auto attr = schema.find("minLength");
91+
if (attr != schema.end())
92+
if (instance.get<std::string>().size() < attr.value()) {
93+
std::ostringstream s;
94+
s << "'" << name << "' of value '" << instance << "' is too short as per minLength ("
95+
<< attr.value() << ")";
96+
throw std::out_of_range(s.str());
97+
}
98+
99+
attr = schema.find("maxLength");
100+
if (attr != schema.end())
101+
if (instance.get<std::string>().size() > attr.value()) {
102+
std::ostringstream s;
103+
s << "'" << name << "' of value '" << instance << "' is too long as per maxLength ("
104+
<< attr.value() << ")";
105+
throw std::out_of_range(s.str());
106+
}
107+
}
108+
109+
void validate_boolean(json & /*instance*/, const json &schema, const std::string &name)
110+
{
111+
validate_type(schema, "boolean", name);
112+
}
113+
114+
void validate_numeric(json &instance, const json &schema, const std::string &name)
115+
{
116+
double value = instance;
117+
118+
const auto &multipleOf = schema.find("multipleOf");
119+
if (multipleOf != schema.end()) {
120+
double rem = fmod(value, multipleOf.value());
121+
if (rem != 0.0)
122+
throw std::out_of_range(name + " is not a multiple ...");
123+
}
124+
125+
const auto &maximum = schema.find("maximum");
126+
if (maximum != schema.end()) {
127+
double maxi = maximum.value();
128+
auto ex = std::out_of_range(name + " exceeds maximum of ...");
129+
if (schema.find("exclusiveMaximum") != schema.end()) {
130+
if (value >= maxi)
131+
throw ex;
132+
} else {
133+
if (value > maxi)
134+
throw ex;
135+
}
136+
}
137+
138+
const auto &minimum = schema.find("minimum");
139+
if (minimum != schema.end()) {
140+
double mini = minimum.value();
141+
auto ex = std::out_of_range(name + " exceeds minimum of ...");
142+
if (schema.find("exclusiveMinimum") != schema.end()) {
143+
if (value <= mini)
144+
throw ex;
145+
} else {
146+
if (value < mini)
147+
throw ex;
148+
}
149+
}
150+
}
151+
152+
void validate_integer(json &instance, const json &schema, const std::string &name)
153+
{
154+
validate_type(schema, "integer", name);
155+
validate_numeric(instance, schema, name);
156+
}
157+
158+
void validate_unsigned(json &instance, const json &schema, const std::string &name)
159+
{
160+
validate_type(schema, "integer", name);
161+
validate_numeric(instance, schema, name);
162+
}
163+
164+
void validate_float(json &instance, const json &schema, const std::string &name)
165+
{
166+
validate_type(schema, "number", name);
167+
validate_numeric(instance, schema, name);
168+
}
169+
170+
void validate_null(json & /*instance*/, const json &schema, const std::string &name)
171+
{
172+
validate_type(schema, "null", name);
173+
}
174+
175+
void validate_array(json & /*instance*/, const json &schema, const std::string &name)
176+
{
177+
not_yet_implemented(schema, "maxItems", "array");
178+
not_yet_implemented(schema, "minItems", "array");
179+
not_yet_implemented(schema, "uniqueItems", "array");
180+
not_yet_implemented(schema, "items", "array");
181+
not_yet_implemented(schema, "additionalItems", "array");
182+
183+
validate_type(schema, "array", name);
184+
}
185+
186+
void validate_object(json &instance, const json &schema, const std::string &name)
187+
{
188+
not_yet_implemented(schema, "maxProperties", "object");
189+
not_yet_implemented(schema, "minProperties", "object");
190+
not_yet_implemented(schema, "dependencies", "object");
191+
192+
validate_type(schema, "object", name);
193+
194+
json properties = {};
195+
if (schema.find("properties") != schema.end())
196+
properties = schema["properties"];
197+
198+
// check for default values of properties
199+
// and insert them into this object, if they don't exists
200+
// works only for object properties for the moment
201+
if (default_value_insertion)
202+
for (auto it = properties.begin(); it != properties.end(); ++it) {
203+
204+
const auto &default_value = it.value().find("default");
205+
if (default_value == it.value().end())
206+
continue; /* no default value -> continue */
207+
208+
if (instance.find(it.key()) != instance.end())
209+
continue; /* value is present */
210+
211+
/* create element from default value */
212+
instance[it.key()] = default_value.value();
213+
}
214+
215+
// additionalProperties
216+
enum {
217+
True,
218+
False,
219+
Object
220+
} additionalProperties = True;
221+
222+
const auto &additionalPropertiesVal = schema.find("additionalProperties");
223+
if (additionalPropertiesVal != schema.end()) {
224+
if (additionalPropertiesVal.value().type() == json::value_t::boolean)
225+
additionalProperties = additionalPropertiesVal.value() == true ? True : False;
226+
else
227+
additionalProperties = Object;
228+
}
229+
230+
json patternProperties = {};
231+
if (schema.find("patternProperties") != schema.end())
232+
patternProperties = schema["patternProperties"];
233+
234+
// check all elements in object
235+
for (auto child = instance.begin(); child != instance.end(); ++child) {
236+
std::string child_name = name + "." + child.key();
237+
238+
// is this a property which is described in the schema
239+
const auto &object_prop = properties.find(child.key());
240+
if (object_prop != properties.end()) {
241+
// validate the element with its schema
242+
validate(child.value(), object_prop.value(), child_name);
243+
continue;
244+
}
245+
246+
bool patternProperties_has_matched = false;
247+
for (auto pp = patternProperties.begin();
248+
pp != patternProperties.end(); ++pp) {
249+
std::regex re(pp.key(), std::regex::ECMAScript);
250+
251+
if (std::regex_search(child.key(), re)) {
252+
validate(child.value(), pp.value(), child_name);
253+
patternProperties_has_matched = true;
254+
}
255+
}
256+
if (patternProperties_has_matched)
257+
continue;
258+
259+
switch (additionalProperties) {
260+
case True:
261+
break;
262+
263+
case Object:
264+
validate(child.value(), additionalPropertiesVal.value(), child_name);
265+
break;
266+
267+
case False:
268+
throw std::invalid_argument("unknown property '" + child.key() + "' in object '" + name + "'");
269+
break;
270+
};
271+
}
272+
273+
// check for required elements which are not present
274+
const auto &required = schema.find("required");
275+
if (required == schema.end())
276+
return;
277+
278+
for (const auto &element : required.value()) {
279+
if (instance.find(element) == instance.end()) {
280+
throw std::invalid_argument("required element '" + element.get<std::string>() +
281+
"' not found in object '" + name + "'");
282+
}
283+
}
284+
}
285+
286+
public:
287+
void validate(json &instance, const json &schema, const std::string &name = "root")
288+
{
289+
not_yet_implemented(schema, "allOf", "all");
290+
not_yet_implemented(schema, "anyOf", "all");
291+
not_yet_implemented(schema, "oneOf", "all");
292+
not_yet_implemented(schema, "not", "all");
293+
not_yet_implemented(schema, "definitions", "all");
294+
not_yet_implemented(schema, "$ref", "all");
295+
296+
validate_enum(instance, schema, name);
297+
298+
switch (instance.type()) {
299+
case json::value_t::object:
300+
validate_object(instance, schema, name);
301+
break;
302+
303+
case json::value_t::array:
304+
validate_array(instance, schema, name);
305+
break;
306+
307+
case json::value_t::string:
308+
validate_string(instance, schema, name);
309+
break;
310+
311+
case json::value_t::number_unsigned:
312+
validate_unsigned(instance, schema, name);
313+
break;
314+
315+
case json::value_t::number_integer:
316+
validate_integer(instance, schema, name);
317+
break;
318+
319+
case json::value_t::number_float:
320+
validate_float(instance, schema, name);
321+
break;
322+
323+
case json::value_t::boolean:
324+
validate_boolean(instance, schema, name);
325+
break;
326+
327+
case json::value_t::null:
328+
validate_null(instance, schema, name);
329+
break;
330+
331+
default:
332+
throw std::out_of_range("type '" + schema["type"].get<std::string>() +
333+
"' has no validator yet");
334+
break;
335+
}
336+
}
337+
};
338+
}
339+
340+
#endif /* NLOHMANN_JSON_VALIDATOR_HPP__ */

‎test.sh

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/sh
2+
3+
if [ ! -x "$1" ]
4+
then
5+
exit 1
6+
fi
7+
8+
if [ ! -e "$2" ]
9+
then
10+
exit 1
11+
fi
12+
13+
$1 < $2

0 commit comments

Comments
 (0)
Please sign in to comment.