From d418ad96ad493afb3afe2175908d0abbae24644b Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Wed, 26 Nov 2025 10:42:15 -0300 Subject: [PATCH 1/3] feat: adds support for list of types --- jambo/parser/_type_parser.py | 33 +++++++++++++++++++++- jambo/schema_converter.py | 11 ++++++-- jambo/types/json_schema_type.py | 2 +- tests/test_schema_converter.py | 49 +++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 4 deletions(-) diff --git a/jambo/parser/_type_parser.py b/jambo/parser/_type_parser.py index efca162..10d479a 100644 --- a/jambo/parser/_type_parser.py +++ b/jambo/parser/_type_parser.py @@ -73,9 +73,40 @@ class GenericTypeParser(ABC, Generic[T]): :param kwargs: Additional options for type parsing. :return: A tuple containing the type and its properties. """ - parser = cls._get_impl(properties) + + parser = cls._get_impl( + cls._normalize_properties(properties) + ) return parser().from_properties(name=name, properties=properties, **kwargs) + + @staticmethod + def _normalize_properties(properties: JSONSchema) -> JSONSchema: + """ + Normalizes the properties dictionary to ensure consistent structure. + :param properties: The properties to be normalized. + """ + type_value = properties.pop("type", None) + + if isinstance(type_value, str): + properties["type"] = type_value + return properties + + if isinstance(type_value, list) and len(type_value) == 0: + raise InvalidSchemaException( + "Invalid schema: 'type' list cannot be empty", invalid_field=str(properties) + ) + + + if isinstance(type_value, list) and len(type_value) == 1: + properties["type"] = type_value[0] + return properties + + if isinstance(type_value, list): + properties["anyOf"] = [{"type": t} for t in type_value] + return properties + + return properties @classmethod def _get_impl(cls, properties: JSONSchema) -> type[Self]: diff --git a/jambo/schema_converter.py b/jambo/schema_converter.py index 4a10c2d..c2c8cae 100644 --- a/jambo/schema_converter.py +++ b/jambo/schema_converter.py @@ -1,4 +1,4 @@ -from jambo.exceptions import InvalidSchemaException, UnsupportedSchemaException +from jambo.exceptions import InternalAssertionException, InvalidSchemaException, UnsupportedSchemaException from jambo.parser import ObjectTypeParser, RefTypeParser from jambo.types import JSONSchema, RefCacheDict @@ -135,5 +135,12 @@ class SchemaConverter: """ if "$ref" in schema: return "$ref" + + type_value = schema.get("type") + if isinstance(type_value, list): + raise InternalAssertionException( + "SchemaConverter._get_schema_type: 'type' field should not be a list here." + " This should have been normalized earlier." + ) - return schema.get("type") + return type_value diff --git a/jambo/types/json_schema_type.py b/jambo/types/json_schema_type.py index 3e73387..82b6762 100644 --- a/jambo/types/json_schema_type.py +++ b/jambo/types/json_schema_type.py @@ -42,7 +42,7 @@ JSONSchema = TypedDict( "description": str, "default": JSONType, "examples": List[JSONType], - "type": JSONSchemaType, + "type": JSONSchemaType|List[JSONSchemaType], "enum": List[JSONType], "const": JSONType, "properties": Dict[str, "JSONSchema"], diff --git a/tests/test_schema_converter.py b/tests/test_schema_converter.py index 018c412..01c18be 100644 --- a/tests/test_schema_converter.py +++ b/tests/test_schema_converter.py @@ -998,3 +998,52 @@ class TestSchemaConverter(TestCase): cached_address_model = self.converter.get_cached_ref("address") self.assertIsNotNone(cached_address_model) + + def test_parse_list_type_multiple_values(self): + schema = { + "title": "TestListType", + "type": "object", + "properties": { + "values": { + "type": ["string", "number"] + } + }, + } + + Model = self.converter.build_with_cache(schema) + + obj1 = Model(values="a string") + self.assertEqual(obj1.values, "a string") + + obj2 = Model(values=42) + self.assertEqual(obj2.values, 42) + + def test_parse_list_type_one_value(self): + schema = { + "title": "TestListType", + "type": "object", + "properties": { + "values": { + "type": ["string"] + } + }, + } + + Model = self.converter.build_with_cache(schema) + + obj1 = Model(values="a string") + self.assertEqual(obj1.values, "a string") + + def test_parse_list_type_empty(self): + schema = { + "title": "TestListType", + "type": "object", + "properties": { + "values": { + "type": [] + } + }, + } + + with self.assertRaises(InvalidSchemaException): + self.converter.build_with_cache(schema) \ No newline at end of file From 40106e4765458db9952684b68dfca307e6df0d56 Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Wed, 26 Nov 2025 10:52:50 -0300 Subject: [PATCH 2/3] feat: validates that top level type cannot be list --- jambo/schema_converter.py | 5 ++--- tests/test_schema_converter.py | 9 +++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/jambo/schema_converter.py b/jambo/schema_converter.py index c2c8cae..3a93bc6 100644 --- a/jambo/schema_converter.py +++ b/jambo/schema_converter.py @@ -138,9 +138,8 @@ class SchemaConverter: type_value = schema.get("type") if isinstance(type_value, list): - raise InternalAssertionException( - "SchemaConverter._get_schema_type: 'type' field should not be a list here." - " This should have been normalized earlier." + raise InvalidSchemaException( + "Invalid schema: 'type' cannot be a list at the top level", invalid_field=str(schema) ) return type_value diff --git a/tests/test_schema_converter.py b/tests/test_schema_converter.py index 01c18be..3bfe327 100644 --- a/tests/test_schema_converter.py +++ b/tests/test_schema_converter.py @@ -1045,5 +1045,14 @@ class TestSchemaConverter(TestCase): }, } + with self.assertRaises(InvalidSchemaException): + self.converter.build_with_cache(schema) + + def test_parse_list_type_root_level_throws(self): + schema = { + "title": "TestListType", + "type": ["string", "number"] + } + with self.assertRaises(InvalidSchemaException): self.converter.build_with_cache(schema) \ No newline at end of file From 27e756dadf77b7fdc7d6129f6b702774bb621d2e Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Wed, 26 Nov 2025 10:54:42 -0300 Subject: [PATCH 3/3] feat: format and linting pre-merge --- jambo/parser/_type_parser.py | 18 ++++++++---------- jambo/schema_converter.py | 7 ++++--- jambo/types/json_schema_type.py | 2 +- tests/test_schema_converter.py | 25 +++++-------------------- 4 files changed, 18 insertions(+), 34 deletions(-) diff --git a/jambo/parser/_type_parser.py b/jambo/parser/_type_parser.py index 10d479a..bcc908b 100644 --- a/jambo/parser/_type_parser.py +++ b/jambo/parser/_type_parser.py @@ -73,13 +73,11 @@ class GenericTypeParser(ABC, Generic[T]): :param kwargs: Additional options for type parsing. :return: A tuple containing the type and its properties. """ - - parser = cls._get_impl( - cls._normalize_properties(properties) - ) + + parser = cls._get_impl(cls._normalize_properties(properties)) return parser().from_properties(name=name, properties=properties, **kwargs) - + @staticmethod def _normalize_properties(properties: JSONSchema) -> JSONSchema: """ @@ -87,21 +85,21 @@ class GenericTypeParser(ABC, Generic[T]): :param properties: The properties to be normalized. """ type_value = properties.pop("type", None) - + if isinstance(type_value, str): properties["type"] = type_value return properties - + if isinstance(type_value, list) and len(type_value) == 0: raise InvalidSchemaException( - "Invalid schema: 'type' list cannot be empty", invalid_field=str(properties) + "Invalid schema: 'type' list cannot be empty", + invalid_field=str(properties), ) - if isinstance(type_value, list) and len(type_value) == 1: properties["type"] = type_value[0] return properties - + if isinstance(type_value, list): properties["anyOf"] = [{"type": t} for t in type_value] return properties diff --git a/jambo/schema_converter.py b/jambo/schema_converter.py index 3a93bc6..37a062d 100644 --- a/jambo/schema_converter.py +++ b/jambo/schema_converter.py @@ -1,4 +1,4 @@ -from jambo.exceptions import InternalAssertionException, InvalidSchemaException, UnsupportedSchemaException +from jambo.exceptions import InvalidSchemaException, UnsupportedSchemaException from jambo.parser import ObjectTypeParser, RefTypeParser from jambo.types import JSONSchema, RefCacheDict @@ -135,11 +135,12 @@ class SchemaConverter: """ if "$ref" in schema: return "$ref" - + type_value = schema.get("type") if isinstance(type_value, list): raise InvalidSchemaException( - "Invalid schema: 'type' cannot be a list at the top level", invalid_field=str(schema) + "Invalid schema: 'type' cannot be a list at the top level", + invalid_field=str(schema), ) return type_value diff --git a/jambo/types/json_schema_type.py b/jambo/types/json_schema_type.py index 82b6762..c720106 100644 --- a/jambo/types/json_schema_type.py +++ b/jambo/types/json_schema_type.py @@ -42,7 +42,7 @@ JSONSchema = TypedDict( "description": str, "default": JSONType, "examples": List[JSONType], - "type": JSONSchemaType|List[JSONSchemaType], + "type": JSONSchemaType | List[JSONSchemaType], "enum": List[JSONType], "const": JSONType, "properties": Dict[str, "JSONSchema"], diff --git a/tests/test_schema_converter.py b/tests/test_schema_converter.py index 3bfe327..450e441 100644 --- a/tests/test_schema_converter.py +++ b/tests/test_schema_converter.py @@ -1003,11 +1003,7 @@ class TestSchemaConverter(TestCase): schema = { "title": "TestListType", "type": "object", - "properties": { - "values": { - "type": ["string", "number"] - } - }, + "properties": {"values": {"type": ["string", "number"]}}, } Model = self.converter.build_with_cache(schema) @@ -1022,11 +1018,7 @@ class TestSchemaConverter(TestCase): schema = { "title": "TestListType", "type": "object", - "properties": { - "values": { - "type": ["string"] - } - }, + "properties": {"values": {"type": ["string"]}}, } Model = self.converter.build_with_cache(schema) @@ -1038,21 +1030,14 @@ class TestSchemaConverter(TestCase): schema = { "title": "TestListType", "type": "object", - "properties": { - "values": { - "type": [] - } - }, + "properties": {"values": {"type": []}}, } with self.assertRaises(InvalidSchemaException): self.converter.build_with_cache(schema) def test_parse_list_type_root_level_throws(self): - schema = { - "title": "TestListType", - "type": ["string", "number"] - } + schema = {"title": "TestListType", "type": ["string", "number"]} with self.assertRaises(InvalidSchemaException): - self.converter.build_with_cache(schema) \ No newline at end of file + self.converter.build_with_cache(schema)