Skip to content

Commit

Permalink
Follow refs when finding default property values
Browse files Browse the repository at this point in the history
This resolves any `ref_schema` keywords (`$ref`, `$dynamicRef`,
`$recursiveRef`) when looking for `default` keywords for
`insert_property_defaults`. It follows the keyword order defined in the
vocabulary (`$ref` first, then `$dynamicRef`/`$recursiveRef` depending
on the meta schema) and searches depth-first (ie, follows a `$ref` chain
until a leaf schema before moving on to a sibling `$dynamicRef`). The
first `default` keyword found is used, meaning a `$ref` default can be
overwritten by the including schema, eg:

```json
{
  "properties": {
    "example": {
      "$ref": "#/$defs/ref",
      "default": "override!"
    }
  },
  "$defs": {
    "ref": {
      "default": "overridden"
    }
  }
}
```

Closes: #173
  • Loading branch information
davishmcclurg committed Feb 25, 2024
1 parent 2eeef77 commit 6a4fe31
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 2 deletions.
16 changes: 14 additions & 2 deletions lib/json_schemer/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@ def insert_property_defaults(context)

if result.source.is_a?(Schema::PROPERTIES_KEYWORD_CLASS) && result.instance.is_a?(Hash)
result.source.parsed.each do |property, schema|
next if result.instance.key?(property) || !schema.parsed.key?('default')
default = schema.parsed.fetch('default')
next if result.instance.key?(property)
next unless default = default_keyword_instance(schema)
instance_location = Location.join(result.instance_location, property)
keyword_location = Location.join(Location.join(result.keyword_location, property), default.keyword)
default_result = default.validate(nil, instance_location, keyword_location, nil)
Expand All @@ -225,5 +225,17 @@ def insert_property_defaults(context)

inserted
end

private

def default_keyword_instance(schema)
schema.parsed.fetch('default') do
schema.parsed.find do |_keyword, keyword_instance|
next unless keyword_instance.respond_to?(:ref_schema)
next unless default = default_keyword_instance(keyword_instance.ref_schema)
break default
end
end
end
end
end
187 changes: 187 additions & 0 deletions test/hooks_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -481,4 +481,191 @@ def test_it_does_not_modify_passed_hooks_array
insert_property_defaults: true
).valid?(data))
end

def test_insert_property_defaults_refs
schema = {
'properties' => {
'inline' => {
'default' => 'a'
},
'ref-inline' => {
'$ref' => '#/$defs/default-b'
},
'ref-inherit' => {
'$ref' => '#/$defs/inherit-default-b'
},
'ref-inline-override' => {
'$ref' => '#/$defs/default-b',
'default' => 'c'
},
'ref-inherit-override' => {
'$ref' => '#/$defs/inherit-default-b',
'default' => 'd'
},
'ref-override' => {
'$ref' => '#/$defs/override-default-e'
}
},
'$defs' => {
'default-b' => {
'default' => 'b'
},
'inherit-default-b' => {
'$ref' => '#/$defs/default-b'
},
'override-default-e' => {
'$ref' => '#/$defs/default-b',
'default' => 'e'
}
}
}
data = {}
assert(JSONSchemer.schema(schema, insert_property_defaults: true).valid?(data))
assert_equal('a', data.fetch('inline'))
assert_equal('b', data.fetch('ref-inline'))
assert_equal('b', data.fetch('ref-inherit'))
assert_equal('c', data.fetch('ref-inline-override'))
assert_equal('d', data.fetch('ref-inherit-override'))
assert_equal('e', data.fetch('ref-override'))
end

def test_insert_property_defaults_dynamic_refs
schema = {
'properties' => {
'dynamic-ref-inline' => {
'$dynamicRef' => '#default-b'
},
'dynamic-ref-inline-override' => {
'$dynamicRef' => '#default-b',
'default' => 'c'
},
'dynamic-ref-and-ref' => {
'$dynamicRef' => '#default-b',
'$ref' => '#/$defs/default-a'
}
},
'$defs' => {
'default-a' => {
'default' => 'a'
},
SecureRandom.uuid => {
'$dynamicAnchor' => 'default-b',
'default' => 'b'
}
}
}
data = {}
assert(JSONSchemer.schema(schema, insert_property_defaults: true).valid?(data))
assert_equal('b', data.fetch('dynamic-ref-inline'))
assert_equal('c', data.fetch('dynamic-ref-inline-override'))
assert_equal('a', data.fetch('dynamic-ref-and-ref'))
end

def test_insert_property_defaults_recursive_refs
schema = {
'$recursiveAnchor' => true,
'default' => 'b',
'properties' => {
'recursive-ref-inline' => {
'$recursiveRef' => '#'
},
'recursive-ref-inline-override' => {
'$recursiveRef' => '#',
'default' => 'c'
},
'recursive-ref-and-ref' => {
'$recursiveRef' => '#',
'$ref' => '#/$defs/default-a'
}
},
'$defs' => {
'default-a' => {
'default' => 'a'
}
}
}
data = {}
assert(JSONSchemer.schema(schema, meta_schema: JSONSchemer.draft201909, insert_property_defaults: true).valid?(data))
assert_equal('b', data.fetch('recursive-ref-inline'))
assert_equal('c', data.fetch('recursive-ref-inline-override'))
assert_equal('a', data.fetch('recursive-ref-and-ref'))
end

def test_insert_property_defaults_non_ref_schema_keywords
schema = {
'properties' => {
'x' => {
'$anchor' => 'x', # parsed before `$ref`
'$ref' => '#/$defs/y',
'type' => 'string' # parsed after `$ref`
}
},
'$defs' => {
'y' => {
'default' => 'z'
}
}
}
data = {}
assert(JSONSchemer.schema(schema, insert_property_defaults: true).valid?(data))
assert_equal('z', data.fetch('x'))
refute(JSONSchemer.schema(schema, insert_property_defaults: true).valid?({ 'x' => 1 }))
end

def test_insert_property_defaults_ref_no_default
schema = {
'properties' => {
'x' => {
'$ref' => '#/$defs/y',
'$dynamicRef' => '#z'
}
},
'$defs' => {
'y' => {
'type' => 'string'
},
SecureRandom.uuid => {
'$dynamicAnchor' => 'z',
'default' => 'dynamic-ref'
}
}
}
data = {}
assert(JSONSchemer.schema(schema, insert_property_defaults: true).valid?(data))
assert_equal('dynamic-ref', data.fetch('x'))
refute(JSONSchemer.schema(schema, insert_property_defaults: true).valid?({ 'x' => 1 }))
end

def test_insert_property_defaults_ref_depth_first
schema = {
'properties' => {
'x' => {
'$ref' => '#/$defs/ref1',
'$dynamicRef' => '#dynamic-ref1'
}
},
'$defs' => {
'ref1' => {
'$ref' => '#/$defs/ref2'
},
'ref2' => {
'$ref' => '#/$defs/ref3'
},
'ref3' => {
'default' => 'ref'
},
SecureRandom.uuid => {
'$dynamicAnchor' => 'dynamic-ref1',
'$dynamicRef' => '#dynamic-ref2'
},
SecureRandom.uuid => {
'$dynamicAnchor' => 'dynamic-ref2',
'default' => 'dynamic-ref'
}
}
}
data = {}
assert(JSONSchemer.schema(schema, insert_property_defaults: true).valid?(data))
assert_equal('ref', data.fetch('x'))
end
end

0 comments on commit 6a4fe31

Please sign in to comment.