Skip to content

Commit 9f45f13

Browse files
committed
tuple fixes
Signed-off-by: Clemens Vasters <clemens@vasters.com>
1 parent ee37da6 commit 9f45f13

File tree

5 files changed

+207
-9
lines changed

5 files changed

+207
-9
lines changed

json-structure-primer.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ updated to conform to the new rules.
9999
declared type. It's a more compact alternative to objects where the
100100
property names are replaced by the position in the tuple. Arrays or maps
101101
of tuples are especially useful for time-series data.
102+
- **Choice:** A discriminated union of types, where one of the types is
103+
selected based on a discriminator property. The `choice` keyword is used
104+
to define the union and the `selector` property is used to specify the
105+
discriminator property.
102106
- **Namespaces:** Namespaces are a formal part of the schema language, allowing
103107
for more modular and deterministic schema definitions. Namespaces are used to
104108
scope type definitions.
@@ -610,6 +614,107 @@ elements. The schema above would match the following instance data:
610614
["apple", "banana", "cherry"]
611615
```
612616

617+
### 4.6. Example: Declaring Tuples
618+
619+
Tuples are fixed-length arrays with named elements, declared via the `tuple` keyword.
620+
621+
```json
622+
{
623+
"$schema": "https://json-structure.org/meta/core/v0/#",
624+
"type": "tuple",
625+
"name": "PersonTuple",
626+
"properties": {
627+
"firstName": { "type": "string" },
628+
"age": { "type": "int32" }
629+
},
630+
"tuple": ["firstName", "age"]
631+
}
632+
```
633+
634+
An instance of this tuple type:
635+
636+
```json
637+
["Alice", 30]
638+
```
639+
640+
### 4.7. Example: Declaring Choice Types
641+
642+
Choice types define discriminated unions via the `choice` keyword. Two forms are supported:
643+
644+
#### Tagged Unions
645+
646+
Tagged unions represent the selected type as a single-property object:
647+
648+
```json
649+
{
650+
"$schema": "https://json-structure.org/meta/core/v0/#",
651+
"type": "choice",
652+
"name": "StringOrNumber",
653+
"choices": {
654+
"string": { "type": "string" },
655+
"int32": { "type": "int32" }
656+
}
657+
}
658+
```
659+
660+
Valid instances:
661+
662+
```json
663+
{ "string": "Hello" }
664+
{ "int32": 42 }
665+
```
666+
667+
#### Inline Unions
668+
669+
Inline unions extend a common abstract base type and use a selector property:
670+
671+
```json
672+
{
673+
"$schema": "https://json-structure.org/meta/core/v0/#",
674+
"type": "choice",
675+
"name": "AddressChoice",
676+
"$extends": "#/definitions/Address",
677+
"selector": "addressType",
678+
"choices": {
679+
"StreetAddress": { "$ref": "#/definitions/StreetAddress" },
680+
"PostOfficeBoxAddress": { "$ref": "#/definitions/PostOfficeBoxAddress" }
681+
},
682+
"definitions": {
683+
"Address": {
684+
"abstract": true,
685+
"type": "object",
686+
"properties": {
687+
"city": { "type": "string" },
688+
"state":{ "type": "string" },
689+
"zip": { "type": "string" }
690+
}
691+
},
692+
"StreetAddress": {
693+
"type": "object",
694+
"$extends": "#/definitions/Address",
695+
"properties": { "street": { "type": "string" } }
696+
},
697+
"PostOfficeBoxAddress": {
698+
"type": "object",
699+
"$extends": "#/definitions/Address",
700+
"properties": { "poBox": { "type": "string" } }
701+
}
702+
}
703+
}
704+
```
705+
706+
Instance of this inline union:
707+
708+
```json
709+
{
710+
"addressType": "StreetAddress",
711+
"street": "123 Main St",
712+
"city": "AnyCity",
713+
"state": "AS",
714+
"zip": "11111"
715+
}
716+
```
717+
613718
## 5. Using Companion Specifications
614719

615720
The JSON Structure Core specification is designed to be extensible through

samples/py/json_structure_instance_validator.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -353,13 +353,25 @@ def validate_instance(self, instance, schema=None, path="#", meta=None):
353353
if not isinstance(instance, list):
354354
self.errors.append(f"Expected tuple (array) at {path}, got {type(instance).__name__}")
355355
else:
356+
# Retrieve the tuple ordering
357+
order = schema.get("tuple")
356358
props = schema.get("properties", {})
357-
expected_len = len(props)
358-
if len(instance) != expected_len:
359-
self.errors.append(f"Tuple at {path} length {len(instance)} does not equal expected {expected_len}")
359+
if order is None:
360+
self.errors.append(f"Tuple schema at {path} is missing the required 'tuple' keyword for ordering")
361+
elif not isinstance(order, list):
362+
self.errors.append(f"'tuple' keyword at {path} must be an array of property names")
360363
else:
361-
for (prop, prop_schema), item in zip(props.items(), instance):
362-
self.validate_instance(item, prop_schema, f"{path}/{prop}")
364+
# Verify each name in order exists in properties
365+
for prop_name in order:
366+
if prop_name not in props:
367+
self.errors.append(f"Tuple order key '{prop_name}' at {path} not defined in properties")
368+
expected_len = len(order)
369+
if len(instance) != expected_len:
370+
self.errors.append(f"Tuple at {path} length {len(instance)} does not equal expected {expected_len}")
371+
else:
372+
for idx, prop_name in enumerate(order):
373+
prop_schema = props[prop_name]
374+
self.validate_instance(instance[idx], prop_schema, f"{path}/{prop_name}")
363375
elif schema_type == "choice":
364376
if not isinstance(instance, dict):
365377
self.errors.append(f"Expected choice object at {path}, got {type(instance).__name__}")

samples/py/json_structure_schema_validator.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,17 @@ def _check_map_schema(self, obj, path):
421421
def _check_tuple_schema(self, obj, path):
422422
"""
423423
Checks constraints for a 'tuple' type.
424-
"""
424+
A valid tuple schema must:
425+
- Include a 'name' attribute.
426+
- Have a 'properties' object where each key is a valid identifier.
427+
- Include a 'tuple' keyword that is an array of strings defining the order.
428+
- Ensure that every element in the 'tuple' array corresponds to a property in 'properties'.
429+
"""
430+
# Check that 'name' is present.
431+
if "name" not in obj:
432+
self._err("Tuple type must include a 'name' attribute.", path + "/name")
433+
434+
# Validate properties.
425435
if "properties" not in obj:
426436
self._err("Tuple type must have 'properties'.", path + "/properties")
427437
else:
@@ -436,6 +446,20 @@ def _check_tuple_schema(self, obj, path):
436446
self._validate_schema(prop_schema, is_root=False, path=f"{path}/properties/{prop_name}")
437447
else:
438448
self._err(f"Tuple property '{prop_name}' must be an object (a schema).", path + f"/properties/{prop_name}")
449+
450+
# Check that the 'tuple' keyword is present.
451+
if "tuple" not in obj:
452+
self._err("Tuple type must include the 'tuple' keyword defining the order of elements.", path + "/tuple")
453+
else:
454+
tuple_order = obj["tuple"]
455+
if not isinstance(tuple_order, list):
456+
self._err("'tuple' keyword must be an array of strings.", path + "/tuple")
457+
else:
458+
for idx, element in enumerate(tuple_order):
459+
if not isinstance(element, str):
460+
self._err(f"Element at index {idx} in 'tuple' array must be a string.", path + f"/tuple[{idx}]")
461+
elif "properties" in obj and isinstance(obj["properties"], dict) and element not in obj["properties"]:
462+
self._err(f"Element '{element}' in 'tuple' does not correspond to any property in 'properties'.", path + f"/tuple[{idx}]")
439463

440464
def _check_choice_schema(self, obj, path):
441465
"""

samples/py/test_json_structure_instance_validator.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,8 @@ def test_tuple_valid():
556556
"properties": {
557557
"first": {"type": "string"},
558558
"second": {"type": "number"}
559-
}
559+
},
560+
"tuple": ["first", "second"]
560561
}
561562
instance = ["hello", 42]
562563
validator = JSONStructureInstanceValidator(schema)
@@ -573,7 +574,8 @@ def test_tuple_wrong_length():
573574
"properties": {
574575
"first": {"type": "string"},
575576
"second": {"type": "number"}
576-
}
577+
},
578+
"tuple": ["first", "second"]
577579
}
578580
instance = ["only one"]
579581
validator = JSONStructureInstanceValidator(schema)

samples/py/test_json_structure_schema_validator.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,19 @@
101101
"name": "EmptyNamespace"
102102
}
103103

104+
# Case 8: Valid tuple type with implicit required properties and tuple order
105+
VALID_TUPLE = {
106+
"$schema": "https://json-structure.org/meta/core/v0/#",
107+
"$id": "https://example.com/schema/tuple",
108+
"name": "PersonTuple",
109+
"type": "tuple",
110+
"properties": {
111+
"name": {"type": "string"},
112+
"age": {"type": "int32"}
113+
},
114+
"tuple": ["name", "age"]
115+
}
116+
104117
VALID_SCHEMAS = [
105118
VALID_MINIMAL,
106119
VALID_OBJECT,
@@ -109,6 +122,7 @@
109122
VALID_EXTENDS,
110123
VALID_ALLOW_DOLLAR, # must be validated with allow_dollar=True
111124
VALID_NAMESPACE_EMPTY,
125+
VALID_TUPLE,
112126
]
113127

114128
# =============================================================================
@@ -332,6 +346,44 @@
332346
"$schema": 123,
333347
}
334348

349+
# Case 26: Tuple type missing 'tuple' keyword.
350+
INVALID_TUPLE_MISSING_KEYWORD = {
351+
"$schema": "https://json-structure.org/meta/core/v0/#",
352+
"$id": "https://example.com/schema/invalid_tuple_missing",
353+
"name": "MissingTupleKeyword",
354+
"type": "tuple",
355+
"properties": {
356+
"name": {"type": "string"},
357+
"age": {"type": "int32"}
358+
}
359+
}
360+
361+
# Case 27: 'tuple' keyword is not an array of strings.
362+
INVALID_TUPLE_NOT_ARRAY = {
363+
"$schema": "https://json-structure.org/meta/core/v0/#",
364+
"$id": "https://example.com/schema/invalid_tuple_not_array",
365+
"name": "TupleNotArray",
366+
"type": "tuple",
367+
"properties": {
368+
"name": {"type": "string"},
369+
"age": {"type": "int32"}
370+
},
371+
"tuple": "name,age"
372+
}
373+
374+
# Case 28: 'tuple' contains non-string elements.
375+
INVALID_TUPLE_NONSTRING_ITEM = {
376+
"$schema": "https://json-structure.org/meta/core/v0/#",
377+
"$id": "https://example.com/schema/invalid_tuple_nonstring",
378+
"name": "TupleNonString",
379+
"type": "tuple",
380+
"properties": {
381+
"name": {"type": "string"},
382+
"age": {"type": "int32"}
383+
},
384+
"tuple": ["name", 42]
385+
}
386+
335387
INVALID_SCHEMAS = [
336388
INVALID_MISSING_SCHEMA,
337389
INVALID_MISSING_ID,
@@ -357,7 +409,10 @@
357409
INVALID_DEFS_ROOT_TYPE,
358410
INVALID_NOT_OBJECT,
359411
INVALID_NOT_ABSOLUTE_URI,
360-
INVALID_NOT_STRING
412+
INVALID_NOT_STRING,
413+
INVALID_TUPLE_MISSING_KEYWORD,
414+
INVALID_TUPLE_NOT_ARRAY,
415+
INVALID_TUPLE_NONSTRING_ITEM
361416
]
362417

363418
# =============================================================================

0 commit comments

Comments
 (0)