Skip to content

Commit

Permalink
"required" JSON keys are validated against "additionalProperties" if …
Browse files Browse the repository at this point in the history
…they are missing from "properties" (#1029)

When `required` properties don't have a
schema in `properties`, validate against `additionalProperties`
  • Loading branch information
hudson-ai authored Sep 18, 2024
1 parent edd8b87 commit af63e6d
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 8 deletions.
24 changes: 16 additions & 8 deletions guidance/library/_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,16 +447,24 @@ def _gen_json_object(
required: Sequence[str],
definitions: Mapping[str, Callable[[], GrammarFunction]],
):
if any(k not in properties for k in required):
raise ValueError(f"Required properties not in properties: {set(required) - set(properties)}")

grammars = tuple(f'"{name}":' + _gen_json(json_schema=schema, definitions=definitions) for name, schema in properties.items())
required_items = tuple(name in required for name in properties)
# "required" keys will be validated against "properties" if they're present, otherwise against "additionalProperties".
# If "additionalProperties" is False, then required keys must be in "properties".
if any(k not in properties for k in required) and additional_properties is False:
raise ValueError(
f"Required properties not in properties but additionalProperties is False."
f" Missing required properties: {list(r for r in required if r not in properties)}"
)
items = [
# First iterate over the properties in order
*properties.items(),
# If there are any keys in required that weren't specified by properties, add them in order at the end,
# where we will validate against the additional_properties schema
*((key, additional_properties) for key in required if key not in properties),
]
grammars = tuple(f'"{name}":' + _gen_json(json_schema=schema, definitions=definitions) for name, schema in items)
required_items = tuple(name in required for name, _ in items)

if additional_properties is not False:
if additional_properties is True:
# True means that anything goes
additional_properties = {}
additional_item_grammar = _gen_json_string() + ':' + _gen_json(json_schema=additional_properties, definitions=definitions)
additional_items_grammar = sequence(additional_item_grammar + ',') + additional_item_grammar
grammars += (additional_items_grammar,)
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/library/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,36 @@ def test_bad_object(self, bad_string, good_bytes, failure_byte, allowed_bytes, m
)


class TestObjectWithMissingRequired:
def test_required_is_required(self):
schema = {"type": "object", "properties": {"a": {"type": "integer"}}, "required": ["b"]}
generate_and_check({"b": 1}, schema)
generate_and_check({"a": 1, "b": "xyz"}, schema)
check_match_failure(
bad_string=_to_compact_json(
{"a": 1}
),
schema_obj=schema,
)

def test_validated_against_additionalProperties(self):
schema = {"type": "object", "properties": {"a": {"type": "integer"}}, "required": ["b"], "additionalProperties": {"type": "integer"}}
generate_and_check({"b": 1}, schema)
generate_and_check({"a": 1, "b": 42}, schema)
check_match_failure(
bad_string=_to_compact_json(
{"a": 1, "b": "string"}
),
schema_obj=schema,
)

def test_false_additionalProperties_fails(self):
schema = {"type": "object", "properties": {"a": {"type": "integer"}}, "required": ["b", "c"], "additionalProperties": False}
with pytest.raises(ValueError) as ve:
_ = gen_json(schema=schema)
assert ve.value.args[0] == "Required properties not in properties but additionalProperties is False. Missing required properties: ['b', 'c']"


class TestSimpleArray:
# These are array without references
@pytest.mark.parametrize("target_obj", [[], [0], [34, 56], [1, 2, 3], [9, 8, 7, 6]])
Expand Down

0 comments on commit af63e6d

Please sign in to comment.