Skip to content

Latest commit

 

History

History
656 lines (568 loc) · 20.9 KB

jsonschema.md

File metadata and controls

656 lines (568 loc) · 20.9 KB

jsonschema extension

Since 0.174.0, the jsonschema extension implements Drafts 4, 6, 7, 2019-9 and 2020-12 of the JSON Schema Specification. Previous versions supported Draft 7.

The documentation below describes the new features for the jsonschema extension since 0.174.0. For earlier releases, please refer to jsonschema (until 0.174.0).

Classes

json_schema A json_schema represents the compiled form of a JSON Schema document.
evaluation_options Allows configuration of JSON Schema evaluation.
validation_message A message type for reporting errors generated by a keyword.
schema_version Supported JSON Schema dialects.
json_validator JSON Schema validator. Deprecated (since 0.174.0)

Functions

make_json_schema Processes a JSON Schema document and returns the compiled form as a json_schema (since 0.174.0).
make_schema Loads a JSON Schema and returns a shared pointer to a json_schema. Deprecated (since 0.174.0)

Compliance

Keywords

The jsoncons implementation passes all required tests in the JSON Schema Test Suite for the keywords below.

Keyword Draft 4 Draft 6 Draft 7 Draft 2019-09 Draft 2020-12
$anchor 🟢 🟢
$defs 🟢 🟢
$dynamicAnchor 🟢
$dynamicRef 🟢
$id 🟢 🟢 🟢 🟢 🟢
$recursiveAnchor 🟢
$recursiveRef 🟢
$ref 🟢 🟢 🟢 🟢 🟢
$vocabulary 🟢 🟢
additionalItems 🟢 🟢 🟢 🟢 🟢
additionalProperties 🟢 🟢 🟢 🟢 🟢
allOf 🟢 🟢 🟢 🟢 🟢
anyOf 🟢 🟢 🟢 🟢 🟢
const 🟢 🟢 🟢 🟢
contains 🟢 🟢 🟢 🟢
contentEncoding 🟢 🟢 🟢
contentMediaType 🟢 🟢 🟢
definitions 🟢 🟢 🟢
dependencies 🟢 🟢 🟢
dependentRequired 🟢 🟢
dependentSchemas 🟢 🟢
enum 🟢 🟢 🟢 🟢 🟢
exclusiveMaximum (boolean) 🟢
exclusiveMaximum 🟢 🟢 🟢 🟢
exclusiveMinimum (boolean) 🟢
exclusiveMinimum 🟢 🟢 🟢 🟢
if-then-else 🟢 🟢 🟢
items 🟢 🟢 🟢 🟢 🟢
maxContains 🟢 🟢
minContains 🟢 🟢
maximum 🟢 🟢 🟢 🟢 🟢
maxItems 🟢 🟢 🟢 🟢 🟢
maxLength 🟢 🟢 🟢 🟢 🟢
maxProperties 🟢 🟢 🟢 🟢 🟢
minimum 🟢 🟢 🟢 🟢 🟢
minItems 🟢 🟢 🟢 🟢 🟢
minLength 🟢 🟢 🟢 🟢 🟢
minProperties 🟢 🟢 🟢 🟢 🟢
multipleOf 🟢 🟢 🟢 🟢 🟢
not 🟢 🟢 🟢 🟢 🟢
oneOf 🟢 🟢 🟢 🟢 🟢
pattern 🟢 🟢 🟢 🟢 🟢
patternProperties 🟢 🟢 🟢 🟢 🟢
prefixItems 🟢
properties 🟢 🟢 🟢 🟢 🟢
propertyNames 🟢 🟢 🟢 🟢
required 🟢 🟢 🟢 🟢 🟢
type 🟢 🟢 🟢 🟢 🟢
unevaluatedItems 🟢 🟢
unevaluatedProperties 🟢 🟢
uniqueItems 🟢 🟢 🟢 🟢 🟢

Format

The implementation understands the following formats:

Format Draft 4 Draft 6 Draft 7 Draft 2019-09 Draft 2020-12
date 🟢 🟢 🟢 🟢
date-time 🟢 🟢 🟢 🟢 🟢
email 🟢 🟢 🟢 🟢 🟢
hostname 🟢 🟢 🟢 🟢 🟢
ipv4 🟢 🟢 🟢 🟢 🟢
ipv6 🟢 🟢 🟢 🟢 🟢
json-pointer 🟢 🟢 🟢 🟢
regex 🟢 🟢 🟢 🟢 🟢
time 🟢 🟢 🟢 🟢

Any other format type is ignored.

Since Draft 2019-09, format is no longer an assertion by default. It can be configured to be an assertion by setting the evaluation option require_format_validation to true

Default values

The JSON Schema Specification includes the "default" keyword for specifying a default value, but doesn't prescribe how implementations should use it during validation. Some implementations ignore the default keyword, others support updating the input JSON to fill in a default value for a missing key/value pair. This implementation outputs a JSONPatch document that may be further applied to the input JSON to add the missing key/value pairs.

Examples

The example schemas are from JSON Schema Miscellaneous Examples, the JSON Schema Test Suite, and user contributions.

Three ways of validating
Format validation
Using a URIResolver to resolve references to schemas defined in external files
Validate before decoding JSON into C++ class objects
Default values

Three ways of validating

This example illustrates the use of three overloads of the validate function that throw, invoke a callback function, and write to a json_visitor.

#include <jsoncons/json.hpp>
#include <jsoncons_ext/jsonschema/jsonschema.hpp>
#include <iostream>

using jsoncons::ojson;
namespace jsonschema = jsoncons::jsonschema;

int main()
    std::string schema_str = R"(
{
  "$id": "https://example.com/arrays.schema.json",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "description": "A representation of a person, company, organization, or place",
  "type": "object",
  "properties": {
    "fruits": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "vegetables": {
      "type": "array",
      "items": { "$ref": "#/$defs/veggie" }
    }
  },
  "$defs": {
    "veggie": {
      "type": "object",
      "required": [ "veggieName", "veggieLike" ],
      "properties": {
        "veggieName": {
          "type": "string",
          "description": "The name of the vegetable."
        },
        "veggieLike": {
          "type": "boolean",
          "description": "Do I like this vegetable?"
        }
      }
    }
  }
}
  )";

    std::string data_str = R"(
{
  "fruits": [ "apple", "orange", "pear" ],
  "vegetables": [
    {
      "veggieName": "potato",
      "veggieLike": true
    },
    {
      "veggieName": "broccoli",
      "veggieLike": "false"
    },
    {
      "veggieName": "carrot",
      "veggieLike": false
    },
    {
      "veggieName": "Swiss Chard"
    }
  ]
}
    )";

    ojson schema = ojson::parse(schema_str);
    jsonschema::json_schema<ojson> compiled = jsonschema::make_json_schema(std::move(schema));
    ojson data = ojson::parse(data_str);
        
    std::cout << "\n(1) Validate using exceptions\n";
    try
    {
        compiled.validate(data);
    }
    catch (const std::exception& e)
    {
        std::cout << e.what() << "\n";
    }
    
    std::cout << "\n(2) Validate using reporter callback\n";
    auto reporter = [](const jsonschema::validation_message& message) -> jsonschema::walk_result
        {
            std::cout << message.instance_location().string() << ": " << message.message() << "\n";
            return jsonschema::walk_result::advance;
        };
    compiled.validate(data, reporter);
    
    std::cout << "\n(3) Validate outputting to a json decoder\n";
    jsoncons::json_decoder<ojson> decoder;
    compiled.validate(data, decoder);
    ojson output = decoder.get_result();
    std::cout << pretty_print(output) << "\n";
}

Output:

(1) Validate using exceptions
/vegetables/1/veggieLike: Expected boolean, found string

(2) Validate using reporter callback
/vegetables/1/veggieLike: Expected boolean, found string
/vegetables/3: Required property 'veggieLike' not found.

(3) Validate outputting to a json decoder
[
    {
        "valid": false,
        "evaluationPath": "/properties/vegetables/items/$ref/properties/veggieLike/type",
        "schemaLocation": "https://example.com/arrays.schema.json#/$defs/veggie/properties/veggieLike",
        "instanceLocation": "/vegetables/1/veggieLike",
        "error": "Expected boolean, found string"
    },
    {
        "valid": false,
        "evaluationPath": "/properties/vegetables/items/$ref/required",
        "schemaLocation": "https://example.com/arrays.schema.json#/$defs/veggie/required",
        "instanceLocation": "/vegetables/3",
        "error": "Required property 'veggieLike' not found."
    }
]

Format validation

Since Draft 2019-09, format validation is disabled by default, but may be enabled by setting the evaluation option require_format_validation to true.

#include <jsoncons/json.hpp>
#include <jsoncons_ext/jsonschema/jsonschema.hpp>
#include <iostream>

using jsoncons::json;
using jsoncons::ojson;
namespace jsonschema = jsoncons::jsonschema;

int main()
{
    json schema = json::parse(R"(
{
    "$id": "/test_schema",
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "properties": {
        "Date": {
            "format": "date-time",
            "type": "string"
        }
    },
    "required": [
        "Date"
    ],
    "type": "object",
    "unevaluatedProperties": false
}
    )");

    auto compiled = jsoncons::jsonschema::make_json_schema(schema,
        jsonschema::evaluation_options{}.require_format_validation(true));

    json data = json::parse(R"(
{ "Date" : "2024-03-19T26:34:56Z" }
    )");

    jsoncons::json_decoder<ojson> decoder;
    compiled.validate(data, decoder);
    ojson output = decoder.get_result();
    std::cout << pretty_print(output) << "\n";
}

Output:

[
    {
        "valid": false,
        "evaluationPath": "/properties/Date/format",
        "schemaLocation": "/test_schema#/properties/Date/format",
        "instanceLocation": "/Date",
        "error": "'2024-03-19T26:34:56Z' is not a RFC 3339 date-time string."
    },
    {
        "valid": false,
        "evaluationPath": "/unevaluatedProperties/Date",
        "schemaLocation": "/test_schema",
        "instanceLocation": "/Date",
        "error": "Unevaluated property 'Date' but the schema does not allow unevaluated properties."
    }
]

Using a URIResolver to resolve references to schemas defined in external files

In this example, the main schema defines a reference using the $ref property to a second schema, defined in an external file name-defs.json,

{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$defs": {
        "orNull": {
            "anyOf": [
                {
                    "type": "null"
                },
                {
                    "$ref": "#"
                }
            ]
        }
    },
    "type": "string"
}

jsoncons needs to know how to turn a URI reference to name-defs.json into a JSON Schema document, and for that it needs you to provide a URIResolver.

#include <jsoncons/json.hpp>
#include <jsoncons_ext/jsonschema/jsonschema.hpp>
#include <fstream>

// for brevity
using jsoncons::json;
namespace jsonschema = jsoncons::jsonschema; 

// Until 0.174.0, throw a `schema_error` instead of returning json::null() 
json resolver(const jsoncons::uri& uri)
{
    std::cout << "uri: " << uri.string() << ", path: " << uri.path() << "\n\n";

    std::string pathname = "./input/jsonschema/";
    pathname += std::string(uri.path());

    std::fstream is(pathname.c_str());
    if (!is)
    {
        return json::null();
    }

    return json::parse(is);        
}

int main()
{ 
    json schema = json::parse(R"(
{
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "$id": "http://localhost:1234/draft2020-12/object",
            "type": "object",
            "properties": {
                "name": {"$ref": "name-defs.json#/$defs/orNull"}
            }
        }
    )");

    // Data
    json data = json::parse(R"(
{
    "name": {
        "name": null
    }
}
    )");

    try
    {
        // Throws schema_error if JSON Schema compilation fails
        jsonschema::json_schema<json> compiled = jsonschema::make_json_schema(schema, resolver);

        auto reporter = [](const jsonschema::validation_message& msg) -> jsonschema::walk_result
        {
            std::cout << message.instance_location().string() << ": " << message.message() << "\n";
            for (const auto& detail : message.details())
            {
                std::cout << "    "  << detail.message() << "\n";
            }
            return jsonschema::walk_result::advance;
        };

        // Will call reporter for each schema violation
        compiled.validate(data, reporter);
    }
    catch (const std::exception& e)
    {
        std::cout << e.what() << '\n';
    }
}

Output:

uri: http://localhost:1234/draft2020-12/name-defs.json, path: /draft2020-12/name-defs.json

/name: Must be valid against at least one schema, but found no matching schemas
    Expected null, found object
    Expected string, found object

Validate before decoding JSON into C++ class objects

This example illustrates decoding data that validates against "oneOf" into a std::variant.

// for brevity
using jsoncons::json;
namespace jsonschema = jsoncons::jsonschema; 

int main()
{
    std::string schema_str = R"(
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "job",
  "description": "job properties json schema",
  "$defs": {
    "os_properties": {
      "type": "object",
      "properties": {
        "command": {
          "description": "this is the OS command to run",
          "type": "string",
          "minLength": 1
        }
      },
      "required": [ "command" ],
      "additionalProperties": false
    },
    "db_properties": {
      "type": "object",
      "properties": {
        "query": {
          "description": "this is db query to run",
          "type": "string",
          "minLength": 1
        }
      },
      "required": [ "query" ],
      "additionalProperties": false
    },

    "api_properties": {
      "type": "object",
      "properties": {
        "target": {
          "description": "this is api target to run",
          "type": "string",
          "minLength": 1
        }
      },
      "required": [ "target" ],
      "additionalProperties": false
    }
  },

  "type": "object",
  "properties": {
    "name": {
      "description": "name of the flow",
      "type": "string",
      "minLength": 1
    },
    "run": {
      "description": "job run properties",
      "type": "object",
      "oneOf": [

        { "$ref": "#/$defs/os_properties" },
        { "$ref": "#/$defs/db_properties" },
        { "$ref": "#/$defs/api_properties" }

      ]
    }
  },
  "required": [ "name", "run" ],
  "additionalProperties":  false
}
    )";
    
    std::string data_str = R"(
{
    "name": "testing flow", 
    "run" : {
        "command": "some command"    
    }
}
    
    )";
    try
    {
        json schema = json::parse(schema_str);
        json data = json::parse(data_str);

        // Throws schema_error if JSON Schema compilation fails
        jsonschema::json_schema<json> compiled = jsonschema::make_json_schema(schema);

        // Test that input is valid before attempting to decode
        if (compiled.is_valid(data))
        {
            const ns::job_properties v = data.as<ns::job_properties>(); // You don't need to reparse data_str 

            std::string output;
            jsoncons::encode_json_pretty(v, output);
            std::cout << output << std::endl;

            // Verify that output is valid
            json test = json::parse(output);
            assert(compiled.is_valid(test));
        }
        else
        {
            std::cout << "Invalid input\n";
        }
    }
    catch (const std::exception& e)
    {
        std::cout << e.what() << '\n';
    }
}

Output:

{
    "name": "testing flow",
    "run": {
        "command": "some command"
    }
}

Default values

#include <jsoncons/json.hpp>
#include <jsoncons_ext/jsonschema/jsonschema.hpp>
#include <jsoncons_ext/jsonpatch/jsonpatch.hpp>
#include <fstream>

// for brevity
using jsoncons::json;
namespace jsonschema = jsoncons::jsonschema; 
namespace jsonpatch = jsoncons::jsonpatch; 

int main() 
{
    json schema = json::parse(R"(
{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "properties": {
        "bar": {
            "type": "string",
            "minLength": 4,
            "default": "bad"
        }
    }
}
)");

    try
    {
        // Data
        json data = json::parse("{}");

        // will throw schema_error if JSON Schema compilation fails 
        jsonschema::json_schema<json> compiled = jsonschema::make_json_schema(schema, resolver); 

        // will throw a validation_error when a schema violation happens 
        json patch;
        compiled.validate(data, patch); 

        std::cout << "Patch: " << patch << "\n";

        std::cout << "Original data: " << data << "\n";

        jsonpatch::apply_patch(data, patch);

        std::cout << "Patched data: " << data << "\n\n";
    }
    catch (const std::exception& e)
    {
        std::cout << e.what() << "\n";
    }
}

Output:

Patch: [{"op":"add","path":"/bar","value":"bad"}]
Original data: {}
Patched data: {"bar":"bad"}