|
|
@@ -0,0 +1,228 @@
|
|
|
+# JSON Validator
|
|
|
+
|
|
|
+A Header-Only JSON Schema Validator library written in C++20.
|
|
|
+Provides json schema validation compliant with the [json schema][1]
|
|
|
+ specification.
|
|
|
+Is compatible with the following draft versions:
|
|
|
+* [draft2020-12][2]
|
|
|
+* [draft2019-09][3]
|
|
|
+* [draft-07][4]
|
|
|
+* [draft-06][5]
|
|
|
+* [draft-05][6]
|
|
|
+* [draft-04][7]
|
|
|
+* [draft-03][8]
|
|
|
+
|
|
|
+## Building
|
|
|
+
|
|
|
+``` bash
|
|
|
+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`
|
|
|
+
|
|
|
+## Usage
|
|
|
+
|
|
|
+There are three main components for json validation,
|
|
|
+ plus an additional customization point:
|
|
|
+1. The adapter between `jvalidate` and the user's JSON type: `jvalidate::Adapter`
|
|
|
+1. The schema object: `jvalidate::Schema`
|
|
|
+1. The validator object: `jvalidate::Validator`
|
|
|
+1. User-defined constraints: `jvalidate::extension::ConstraintBase` and
|
|
|
+ `jvalidate::extension::Visitor`.
|
|
|
+
|
|
|
+### Creating an Adapter for custom JSON Types
|
|
|
+
|
|
|
+An adapter represents the following interface/contract for
|
|
|
+ a json implementation `MyJsonType`:
|
|
|
+* A free function with the signature
|
|
|
+ `bool load_stream(std::istream &, MyJsonType &, std::string &error) noexcept`
|
|
|
+* An implementation of `jvalidate::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() { ... }`
|
|
|
+* An adapter class implementation that subclasses
|
|
|
+ `jvalidate::adapter::Adapter` and
|
|
|
+ fulfills the contract `jvalidate::Adapter`.
|
|
|
+ * By convention, the adapter class should not have ownership of the
|
|
|
+ underlying object.
|
|
|
+* An adapter class implementation that fulfills the contract
|
|
|
+ `jvalidate::ArrayAdapter`
|
|
|
+* An adapter class implementation that fulfills the contract
|
|
|
+ `jvalidate::ObjectAdapter`
|
|
|
+
|
|
|
+### Constructing a Schema
|
|
|
+
|
|
|
+The 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.
|
|
|
+``` c++
|
|
|
+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.
|
|
|
+
|
|
|
+#### URI Resolver
|
|
|
+
|
|
|
+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`.
|
|
|
+
|
|
|
+#### Constraint Factory
|
|
|
+
|
|
|
+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:
|
|
|
+1. The constructor `ConstraintFactory(std::initializer_list<std::pair<std::string_view, Versioned>> init)`
|
|
|
+1. An append function `ConstraintFactory::with_user_keyword(std::string_view, Versioned)`
|
|
|
+
|
|
|
+For example:
|
|
|
+``` c++
|
|
|
+jvalidate::Schema schema(
|
|
|
+ schema_document, jvalidate::schema::Version::Draft2020_12,
|
|
|
+ jvalidate::ConstraintFactory()
|
|
|
+ .with_user_keyword("my_keyword", create_my_constraint));
|
|
|
+```
|
|
|
+
|
|
|
+<!-- @see jvalidate::ConstraintFactory::Versioned -->
|
|
|
+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}}},
|
|
|
+ ```
|
|
|
+
|
|
|
+### Validating a Document
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+### Creating custom extensions
|
|
|
+
|
|
|
+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:
|
|
|
+``` c++
|
|
|
+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.
|
|
|
+``` c++
|
|
|
+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.
|
|
|
+``` c++
|
|
|
+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.
|
|
|
+
|
|
|
+<!-- Footnotes/Appendix -->
|
|
|
+
|
|
|
+[1]: https://json-schema.org/specification
|
|
|
+[2]: https://json-schema.org/draft/2020-12
|
|
|
+[3]: https://json-schema.org/draft/2019-09
|
|
|
+[4]: https://json-schema.org/draft-07
|
|
|
+[5]: https://json-schema.org/draft-06
|
|
|
+[6]: https://json-schema.org/draft-05
|
|
|
+[7]: https://json-schema.org/draft-04/draft-zyp-json-schema-04
|
|
|
+[8]: https://json-schema.org/draft-03/draft-zyp-json-schema-03.pdf
|