|
|
1 week ago | |
|---|---|---|
| include | 1 week ago | |
| src | 1 week ago | |
| tests | 1 week ago | |
| thirdparty | 2 weeks ago | |
| .clang-format | 1 year ago | |
| .clang-tidy | 1 week ago | |
| .gitignore | 1 year ago | |
| .gitmodules | 2 weeks ago | |
| CMakeLists.txt | 2 weeks ago | |
| LICENSE.md | 3 weeks ago | |
| README.md | 2 weeks ago |
A Header-Only JSON Schema Validator library written in C++20.
Provides json schema validation compliant with the json schema
specification.
Is compatible with the following draft versions:
cmake -S . -B build [options...]
make -C build
ctest --test-dir build/tests
If, for example, you have ICU provided via homebrew, you will need to add the homebrew paths to your cmake command.
For example: -DICU_ROOT=/opt/homebrew/opt/icu4c
There are three main components for json validation, plus an additional customization point:
jvalidate and the user's JSON type: jvalidate::Adapterjvalidate::Schemajvalidate::Validatorjvalidate::extension::ConstraintBase and
jvalidate::extension::Visitor.An adapter represents the following interface/contract for
a json implementation MyJsonType:
bool load_stream(std::istream &, MyJsonType &, std::string &error) noexceptjvalidate::adapter::AdapterTraits<MyJsonType>,
which must provide the following:
template <typename T> using Adapter = MyJsonTypeAdapter<T>using ConstAdapter = MyJsonTypeAdapter<MyJsonType const>static MyJsonType const &const_empty() { ... }jvalidate::adapter::Adapter and
fulfills the contract jvalidate::Adapter.
jvalidate::ArrayAdapterjvalidate::ObjectAdapterThe schema class describes how to validate a json document, but does not contain the business logic to perform the validation.
The simplest constructor for a schema has the following signature,
and uses jvalidate::adapter::AdapterTraits to deduce the adapter
that should wrap the document.
MyJsonType schema_document = ...;
jvalidate::Schema schema(schema_document, jvalidate::schema::Version::Draft2020_12);
Schemas also support additional components, which can be provided in any order.
A URIResolver is a free function with the following signature:
bool (*)(jvalidate::URI const &, MyJsonType &, std::string &error) noexcept.
If no uri resolver is provided, then it is not possible to read schemas from the internet or from files. It is also not possible to read custom vocabularies.
A curl based resolver is provided in include/jvalidate/compat/curl.h.
A ConstraintFactory is the object that maps keywords to Constraint objects. Because it is possible to change schema versions when following a reference, ConstraintFactory MUST provide information on all schema drafts at once.
There are two ways to provide user-defined keywords:
ConstraintFactory(std::initializer_list<std::pair<std::string_view, Versioned>> init)ConstraintFactory::with_user_keyword(std::string_view, Versioned)For example:
jvalidate::Schema schema(
schema_document, jvalidate::schema::Version::Draft2020_12,
jvalidate::ConstraintFactory()
.with_user_keyword("my_keyword", create_my_constraint));
In order to support multiple schema versions in a single instance of a ConstraintFactory, we need to be able to describe which version a keyword becomes part of the language vocabulary, and what (if any) version it leaves the vocabulary after.
To do this, we store an ordered map of Version enum onto a vocabulary Metadata
object and then use std::map::lower_bound to determine which one is the most
appropriate for the schema version being evaluated.
For example:
The "additionalProperties" constraint is the same across all versions, and so can be represented using only a function pointer.
{"additionalProperties", &Self::additionalProperties}
The "const" constraint was not added until Draft06, so we include the version when constructing its constraint bindings like so:
{"const", {schema::Version::Draft06, &Self::isConstant}}
The "divisibleBy" constraint was removed in favor of "multipleOf" in Draft04, and therefore is represented as:
{"divisibleBy", {{schema::Version::Earliest, &Self::multipleOf},
{schema::Version::Draft04, Removed}}},
{"multipleOf", {schema::Version::Draft04, &Self::multipleOf}}
A small number of rare constraints change their meaning when moving from one draft version to another in such a significant way that it makes more sense to use different MakeConstraint functions for them.
{"items", {{schema::Version::Earliest, &Self::itemsTupleOrVector},
{schema::Version::Draft2020_12, &Self::additionalItems}}}
Reserved keywords that have no meaning by themselves can use the Literal rule:
{"contains", {schema::Version::Draft06, &Self::contains}},
{"maxContains", {schema::Version::Draft06, Literal}},
{"minContains", {schema::Version::Draft06, Literal}},
Since some special words like "default", "examples", "enum", etc. may contain objects which should not be evaluated as JSON schemas for things like "$id" tokens, the rules Keyword and KeywordMap allow marking only those keywords that are expected to hold more json schemas to be evaluated:
{"$defs", {schema::Version::Draft2019_09, KeywordMap}},
{"additionalProperties", {{&Self::additionalProperties, Keyword}}},
{"allOf", {schema::Version::Draft04, {&Self::allOf, Keyword}}},
Validation is performed by constructing a Validator object,
and calling the validate() function.
The validator does not own the schema node that it operates on,
but it does own a copy of the provided ExtensionVisitor (if any),
and of the RegexEngine that it maintains internally.
Reusing the Validator can allow for caching of regular expressions,
if they are commonly used in the schema.
There are two main ways to call Validator::validate,
with or without a jvalidate::ValidationResult object.
If validate is called without a result,
then the evaluation will end immediately after
the first rejecting constraint.
Otherwise,
it will return all evaluation reasons.
Custom extensions allow the implementation of user-specific constraints that either cannot naturally be described in the json schema specification or are used commonly enough that the DRY principle applies. Some virtual function magic is used to unwrap all of the type-erasure that occurs does not require the consumer to write any boilerplate.
A user constraint is defined as a simple struct with the following form:
struct MyCustomConstraint : jvalidate::extension::ConstraintBase<MyCustomConstraint> {
MyCustomConstraint(...) { ... } // Required b/c we're a subclass
// fields...
};
It is added to the Schema as a new keyword through the ConstraintFactory. Much like builtin constraints, context can be used to extract values from the schema json, or to evaluate child schemas.
jvalidate::ConstraintFactory factory{
{"my_keyword", [](auto const & context) {
return ExtensionConstraint::make<MyCustomConstraint>(...);
}},
};
A validator is created as follows, and then is passed in as the ExtensionVisitor argument to Validator.
class Visitor : jvalidate::extension::Visitor<Visitor, MyCustomConstraint, ...> {
public:
// One of these for every constraint in the template signature
Status visit(MyCustomConstraint const & cons,
jvalidate::Adapter auto const & document,
auto const &validator) const;
};
In tests/extension_test.cxx,
an example demonstrating a json schema for a graph
which requires each edge's source and destination are
nodes in the graph.