danielaparker / jsoncons

A C++, header-only library for constructing JSON and JSON-like data formats, with JSON Pointer, JSON Patch, JSON Schema, JSONPath, JMESPath, CSV, MessagePack, CBOR, BSON, UBJSON
https://danielaparker.github.io/jsoncons
Other
697 stars 160 forks source link

Support draft-04 (and other?) JSON schemas #493

Closed mtmorgan closed 4 months ago

mtmorgan commented 6 months ago

Describe the proposed feature

Currently, validation requires schemas following the 'draft-07' schema, but many schemas 'in the wild' are draft-04, an example relevant to jsoncon is JSON patch https://json.schemastore.org/json-patch.json.

What other libraries (C++ or other) have this feature?

I think many other libraries support, to one extent or another, draft-04

Include a code fragment with sample data that illustrates the use of this feature

#include "jsoncons_ext/jsonschema/jsonschema.hpp"

using namespace jsoncons;

int main(int argc, char *argv[])
{
    json schema = json::parse(R"(
{
    "$schema": "http://json-schema.org/draft-04/schema#"
}
    )");
    auto sch = jsonschema::make_schema(schema);
}

leads to

$ ./a.out
libc++abi: terminating due to uncaught exception of type jsoncons::jsonschema::schema_error: Unsupported schema version http://json-schema.org/draft-04/schema#
zsh: abort      ./a.out

Apparently the changes from 04 to 06 are minor and 07 is backward-compatible with 06.

I could develop a pull request, but likely there would be a lot of 'mentoring' ;) involved to make it worth-while.

danielaparker commented 6 months ago

We're currently in the process of restructuring the jsonschema code to accommodate multiple versions. The current focus is on Draft 2019-09, to be followed by 2012-12. I hadn't really thought about it, but I suppose adding 04 and 06 wouldn't be a big deal.

danielaparker commented 4 months ago

make_json_schema on master now supports drafts 04 and 06, in addition to 07, 2019-09, and 2020-12. It will detect the appropriate schema using $schema, if present, or if not, from the evaluation option default_version.

mtmorgan commented 4 months ago

Amazing!

I notice when validating the JSONPatch [{"op": "adds", "paths": "/biscuits/1", "value": { "name": "Ginger Nut" }}] against https://json.schemastore.org/json-patch.json, the details provide errors containing single- ('paths'), double- ("paths"), and unquoted (adds) values / properties; it would be great if this were consistent (with single quotes being my preferred flavor...).

"Required property \"path\" not found"
"adds is not a valid enum value"
"Additional property 'paths' but the schema does not allow additional properties."

Happy to provide a PR...

danielaparker commented 4 months ago

@mtmorgan Feel free to submit a PR with cleaned up error messages, I agree with single quote consistency. The validation messages are mostly constructed in the file jsonschema/common/keyword_validators.hpp. Please take the most recent copy of master, there's been one name change with the new classes, schema -> schema_version.

mtmorgan commented 4 months ago

I notice that

    jsoncons::ojson schema_;
    auto compiled = jsoncons::jsonschema::make_json_schema(schema_);

fails to compile with error

no matching function for call to object of type 'schema_builder_factory<basic_json<char, order_preserving_policy, allocator>>'

No problem for jsoncons::json.

danielaparker commented 4 months ago

@mtmorgan Should be fixed on master

mtmorgan commented 4 months ago

Trying to validate a JSON patch 'op' against the patch schema seems to result in an incorrect 'evaluationPath' with /items/0/oneOf instead of what I think should be /items/oneOf (the 0/ is correctly included in 'details', if I understand correctly).

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

int main(int argc, char *argv[])
{
    std::string schema_string = R"(
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "definitions": {
    "path": {
      "description": "A JSON Pointer path.",
      "type": "string"
    }
  },
  "id": "https://json.schemastore.org/json-patch.json",
  "items": {
    "oneOf": [
      {
        "additionalProperties": false,
        "required": ["value", "op", "path"],
        "properties": {
          "path": {
            "$ref": "#/definitions/path"
          },
          "op": {
            "description": "The operation to perform.",
            "type": "string",
            "enum": ["add", "replace", "test"]
          },
          "value": {
            "description": "The value to add, replace or test."
          }
        }
      },
      {
        "additionalProperties": false,
        "required": ["op", "path"],
        "properties": {
          "path": {
            "$ref": "#/definitions/path"
          },
          "op": {
            "description": "The operation to perform.",
            "type": "string",
            "enum": ["remove"]
          }
        }
      },
      {
        "additionalProperties": false,
        "required": ["from", "op", "path"],
        "properties": {
          "path": {
            "$ref": "#/definitions/path"
          },
          "op": {
            "description": "The operation to perform.",
            "type": "string",
            "enum": ["move", "copy"]
          },
          "from": {
            "$ref": "#/definitions/path",
            "description": "A JSON Pointer path pointing to the location to move/copy from."
          }
        }
      }
    ]
  },
  "title": "JSON schema for JSONPatch files",
  "type": "array"
})";

    std::string data_string = R"([
{
    "op": "invalid_op",
    "path": "/biscuits/1",
    "value":{"name":"Ginger Nut" }
}])";

    auto schema_ = jsoncons::ojson::parse(schema_string);
    auto data_ = jsoncons::ojson::parse(data_string);
    auto compiled = jsoncons::jsonschema::make_json_schema(schema_);
    jsoncons::json_decoder<jsoncons::ojson> decoder;
    compiled.validate(data_, decoder);
    auto output = decoder.get_result();

    std::cout << pretty_print(output) << std::endl;

    return 0;
}

producing in part

...
        "evaluationPath": "/items/0/oneOf", 
...
        "details": [
            {
                "valid": false, 
                "evaluationPath": "/items/0/oneOf/0/additionalProperties/properties/op/enum", 
...
danielaparker commented 4 months ago

Thanks! That observation was very helpful. Should be fixed on master.

mtmorgan commented 4 months ago

I'm not sure the paths are correct yet? I see

{
                "error": "'invalid_op' is not a valid enum value.", 
                "evaluationPath": "/items/oneOf/0/additionalProperties/properties/op/enum", 
                "instanceLocation": "/0/op", 
                "schemaLocation": "https://json.schemastore.org/json-patch.json#/items/oneOf/0/properties/op/enum", 
                "valid": false
            },

but the evaluationPath does not exist in the schema? Or maybe I'm misunderstanding...?

danielaparker commented 4 months ago

I'm not sure either :-) Suspect they aren't.

Anyway that one is a separate issue that should be fixed now on master.