Skip to content

Commit 57a03f3

Browse files
committed
feat: enhance JSON Structure validator with strict object properties validation
- Add validation for object schemas: properties MUST have at least one entry when present - Allow empty properties for schemas using (properties inherited from base) - Fix property merging to properly combine base and derived properties - Add clarifying comment for map key validation (any valid JSON string allowed) - Add comprehensive tests for empty properties validation - Add tests for map keys with various valid JSON string formats - Fix test_extends_valid by improving inheritance property merging BREAKING CHANGE: Object schemas with empty properties are now rejected unless using
1 parent e4da0ef commit 57a03f3

File tree

2 files changed

+79
-4
lines changed

2 files changed

+79
-4
lines changed

samples/py/json_structure_instance_validator.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,7 @@ def validate_instance(self, instance, schema=None, path="#", meta=None):
209209

210210
if not isinstance(schema_type, str):
211211
self.errors.append(f"Schema at {path} has invalid 'type'")
212-
return self.errors
213-
214-
# Process $extends. [Metaschema: $extends in ObjectType/TupleType]
212+
return self.errors # Process $extends. [Metaschema: $extends in ObjectType/TupleType]
215213
if schema_type != "choice" and "$extends" in schema:
216214
base = self._resolve_ref(schema["$extends"])
217215
if base is None:
@@ -225,6 +223,11 @@ def validate_instance(self, instance, schema=None, path="#", meta=None):
225223
f"Property '{key}' is inherited via $extends and must not be redefined at {path}")
226224
merged = dict(base)
227225
merged.update(schema)
226+
# Properly merge properties: base properties + derived properties
227+
if "properties" in base or "properties" in schema:
228+
merged_props = dict(base_props)
229+
merged_props.update(derived_props)
230+
merged["properties"] = merged_props
228231
merged.pop("$extends", None)
229232
merged.pop("abstract", None)
230233
schema = merged
@@ -331,6 +334,14 @@ def validate_instance(self, instance, schema=None, path="#", meta=None):
331334
self.errors.append(f"Expected JSON pointer format at {path}")
332335
# Compound types.
333336
elif schema_type == "object":
337+
# Validate schema: properties MUST have at least one entry if present,
338+
# unless the schema uses $extends (properties may be inherited)
339+
if "properties" in schema:
340+
props_def = schema["properties"]
341+
if not isinstance(props_def, dict) or (len(props_def) == 0 and "$extends" not in schema):
342+
self.errors.append(f"Object schema at {path} has 'properties' but it is empty - properties MUST have at least one entry")
343+
return self.errors
344+
334345
if not isinstance(instance, dict):
335346
self.errors.append(f"Expected object at {path}, got {type(instance).__name__}")
336347
else:
@@ -390,6 +401,7 @@ def validate_instance(self, instance, schema=None, path="#", meta=None):
390401
if not isinstance(instance, dict):
391402
self.errors.append(f"Expected map (object) at {path}, got {type(instance).__name__}")
392403
else:
404+
# Map keys MAY be any valid JSON string (no restrictions on key format)
393405
values_schema = schema.get("values")
394406
if values_schema:
395407
for key, val in instance.items():

samples/py/test_json_structure_instance_validator.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,36 @@ def test_object_additional_properties_false():
484484
assert any("Additional property 'b'" in err for err in errors)
485485

486486

487+
def test_object_empty_properties_invalid():
488+
"""Test that object schema with empty properties is invalid"""
489+
schema = {
490+
"type": "object",
491+
"$schema": "https://json-structure.org/meta/core/v0/#",
492+
"$id": "dummy",
493+
"name": "objSchema",
494+
"properties": {} # Empty properties should be invalid
495+
}
496+
instance = {"a": "value"}
497+
validator = JSONStructureInstanceValidator(schema)
498+
errors = validator.validate_instance(instance)
499+
assert any("properties MUST have at least one entry" in err for err in errors)
500+
501+
502+
def test_object_properties_none_invalid():
503+
"""Test that object schema with properties set to None is invalid"""
504+
schema = {
505+
"type": "object",
506+
"$schema": "https://json-structure.org/meta/core/v0/#",
507+
"$id": "dummy",
508+
"name": "objSchema",
509+
"properties": None # None properties should be invalid
510+
}
511+
instance = {"a": "value"}
512+
validator = JSONStructureInstanceValidator(schema)
513+
errors = validator.validate_instance(instance)
514+
assert any("properties MUST have at least one entry" in err for err in errors)
515+
516+
487517
def test_array_valid():
488518
schema = {
489519
"type": "array",
@@ -554,6 +584,39 @@ def test_map_valid():
554584
assert errors == []
555585

556586

587+
def test_map_keys_any_valid_json_string():
588+
"""Test that map keys MAY be any valid JSON string (lifted restriction)"""
589+
schema = {
590+
"type": "map",
591+
"$schema": "https://json-structure.org/meta/core/v0/#",
592+
"$id": "dummy",
593+
"name": "mapSchema",
594+
"values": {"type": "string"}
595+
}
596+
597+
# Test various valid JSON strings as keys
598+
instance = {
599+
"simple_key": "value1",
600+
"key-with-hyphens": "value2",
601+
"key.with.dots": "value3",
602+
"key with spaces": "value4",
603+
"key/with/slashes": "value5",
604+
"key:with:colons": "value6",
605+
"key@with@symbols": "value7",
606+
"123numeric_start": "value8",
607+
"_underscore_start": "value9",
608+
"UPPERCASE_KEY": "value10",
609+
"CamelCaseKey": "value11",
610+
"key_with_unicode_café": "value12",
611+
"empty": "",
612+
"very_long_key_name_that_exceeds_normal_length_expectations_but_is_still_valid": "value13"
613+
}
614+
615+
validator = JSONStructureInstanceValidator(schema)
616+
errors = validator.validate_instance(instance)
617+
assert errors == [], f"Unexpected validation errors: {errors}"
618+
619+
557620
def test_tuple_valid():
558621
schema = {
559622
"type": "tuple",
@@ -1429,7 +1492,7 @@ def test_import_circular_reference_prevention():
14291492

14301493
schema = {
14311494
"$schema": "https://json-structure.org/meta/core/v0/#",
1432-
"$id": "https://example.com/schema/test",
1495+
"$id": "dummy",
14331496
"name": "TestSchema",
14341497
"type": "object",
14351498
"properties": {

0 commit comments

Comments
 (0)