brandur / json_schema

A JSON Schema V4 and Hyperschema V4 parser and validator.
MIT License
230 stars 45 forks source link

oneOf / nullable ref behavior #100

Closed 97jaz closed 5 years ago

97jaz commented 6 years ago

When I have a property like:

"foo":  {
  "oneOf": [
    { "$ref": "file:/bar.json#" },
    { "type": "null" }
  ]
}

and I try to match against valid data, I'm getting an error telling me More than one subschema in "oneOf" matched. Which would seem to be impossible, unless the $ref is implicitly taken to be nullable. But I didn't think it's supposed to be.

kytrinyx commented 5 years ago

@97jaz I dug into this and it looks like the "file:/bar.json#" is not being expanded.

@brandur I will work on a fix, ~as this is very likely a problem introduced with the changes in #106 and #108.~ Update: probably not a regression, but I have a failing test, so will work on a fix anyway :)

~In the meanwhile, @97jaz, I think your best bet is to pin the json_schema gem version to 0.20.1 for your project.~

kytrinyx commented 5 years ago

@97jaz Can you confirm the exact format of your $ref? I'm not used to seeing a : in the value. (Update: on second thoughts, I'm assuming this is a URI that refers to the file system)

kytrinyx commented 5 years ago

I have the following test (I put it in test/json_schema/reference_expander_test.rb).

Based on your description of the problem, @97jaz, I would expect this to fail, but it is passing. Would you help figure out what is different between the defined fixtures and your use case, in order to get a failing test?

  it "works" do
    sample1 = {
      "$schema" => "http://json-schema.org/draft-04/hyper-schema",
      "type" => "object",
      "properties" => {
        "foo" => {
          "oneOf" => [
            {"type" => "null"},
            {"$ref" => "http://json-schema.org/b.json#"}
          ]
        }
      }
    }
    schema1 = JsonSchema::Parser.new.parse!(sample1)
    schema1.uri = "http://json-schema.org/a.json"

    sample2 = {
      "$schema" => "http://json-schema.org/draft-04/hyper-schema",
      "type" => "object",
      "properties" => {
        "bar" => {
          "type" => "string",
          "maxLength" => 3
        }
      }
    }
    schema2 = JsonSchema::Parser.new.parse!(sample2)
    schema2.uri = "http://json-schema.org/b.json"

    # Initialize a store and add our schema to it.
    store = JsonSchema::DocumentStore.new
    store.add_schema(schema1)
    store.add_schema(schema2)

    expander = JsonSchema::ReferenceExpander.new
    expander.expand(schema1, store: store)

    ok, errors = schema1.validate({"foo" => nil})
    assert ok, "with nil"
    ok, errors = schema1.validate({"foo" => {"bar" => "abc"}})
    assert ok, "with object"
    ok, errors = schema1.validate({"foo" => {"bar" => "abcd"}})
    refute ok, "with invalid value in object"
  end
kytrinyx commented 5 years ago

Ok, I experimented with adding another layer of indirection, and was able to make a failing test.

This suggests that this is not a regression introduced in #106 or #107, but rather an edge case not covered by the current test suite. @97jaz, if this is the case, I don't think pinning the gem to 0.20.1 will help.

  it "debugging brandur/json_schema#100" do
    sample1 = {
      "$schema" => "http://json-schema.org/draft-04/hyper-schema",
      "type" => "object",
      "properties" => {
        "foo" => {
          "$ref" => "http://json-schema.org/b.json#"
        }
      }
    }
    schema1 = JsonSchema::Parser.new.parse!(sample1)
    schema1.uri = "http://json-schema.org/a.json"

    sample2 = {
      "$schema" => "http://json-schema.org/draft-04/hyper-schema",
      "type" => "object",
      "properties" => {
        "bar" => {
          "oneOf" => [
            {"type" => "null"},
            {"$ref" => "http://json-schema.org/c.json#"}
          ]
        }
      },
    }
    schema2 = JsonSchema::Parser.new.parse!(sample2)
    schema2.uri = "http://json-schema.org/b.json"

    sample3 = {
      "$schema" => "http://json-schema.org/draft-04/hyper-schema",
      "type" => "object",
      "properties" => {
        "baz" => {
          "type" => "integer"
        }
      }
    }
    schema3 = JsonSchema::Parser.new.parse!(sample3)
    schema3.uri = "http://json-schema.org/c.json"

    # Initialize a store and add our schema to it.
    store = JsonSchema::DocumentStore.new
    store.add_schema(schema1)
    store.add_schema(schema2)
    store.add_schema(schema3)

    expander = JsonSchema::ReferenceExpander.new
    expander.expand(schema1, store: store)

    ok, errors = schema1.validate({"foo" => {"bar" => nil}})
    assert ok, "should be valid with nil (#{errors.inspect})"
    ok, errors = schema1.validate({"foo" => {"bar" => {"baz" => 1}}})
    assert ok, "should be valid with object (#{errors.inspect})"
  end

If you eyeball schema1 just before the assertions, you'll see:

schema1.properties["foo"].properties["bar"].inspect_schema
=> {:@expanded=>"true", :@one_of=>[{:@expanded=>"true", :@type=>["\"null\""]}, "http://json-schema.org/c.json# [COLLAPSED] [ORIGINAL]"]}
kytrinyx commented 5 years ago

I found another edge case 🎉 (JSON Schema, the gift that keeps giving 😂)

Here's the failing test that I put together:

  it "expands a local reference within the link of a remote schema" do
    sample1 = {
      "$schema" => "http://json-schema.org/draft-04/hyper-schema",
      "type" => "object",
      "properties" => {
        "foo" => {
          "$ref" => "http://json-schema.org/b.json#"
        }
      }
    }
    schema1 = JsonSchema::Parser.new.parse!(sample1)
    schema1.uri = "http://json-schema.org/a.json"

    sample2 = {
      "$schema" => "http://json-schema.org/draft-04/hyper-schema",
      "type" => "object",
      "definitions" => {
        "bar" => {
          "type" => "string",
          "maxLength" => 4
        }
      },
      "links" => [
        {
          "schema" => {
            "properties" => {
              "baz" => {
                "$ref" => "#/definitions/bar"
              }
            }
          }
        }
      ]
    }
    schema2 = JsonSchema::Parser.new.parse!(sample2)
    schema2.uri = "http://json-schema.org/b.json"

    # Initialize a store and add our schema to it.
    store = JsonSchema::DocumentStore.new
    store.add_schema(schema1)
    store.add_schema(schema2)

    expander = JsonSchema::ReferenceExpander.new
    expander.expand!(schema1, store: store)

    assert_equal 4, schema1.properties["foo"].links[0].schema.properties["baz"].max_length