From b6f71ac06084d031680397718d80d685c7dfdeab Mon Sep 17 00:00:00 2001 From: Eduard Stanculet Date: Wed, 28 Jan 2026 13:24:25 +0200 Subject: [PATCH] feat: simple serialization defaults --- pyproject.toml | 2 +- src/uipath/core/serialization/__init__.py | 5 + src/uipath/core/serialization/json.py | 150 ++++++ src/uipath/core/tracing/_utils.py | 58 +- tests/serialization/test_json.py | 614 ++++++++++++++++++++++ uv.lock | 2 +- 6 files changed, 776 insertions(+), 55 deletions(-) create mode 100644 src/uipath/core/serialization/__init__.py create mode 100644 src/uipath/core/serialization/json.py create mode 100644 tests/serialization/test_json.py diff --git a/pyproject.toml b/pyproject.toml index 1c9200d..786d5d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.2.1" +version = "0.2.2" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/src/uipath/core/serialization/__init__.py b/src/uipath/core/serialization/__init__.py new file mode 100644 index 0000000..aa26e75 --- /dev/null +++ b/src/uipath/core/serialization/__init__.py @@ -0,0 +1,5 @@ +"""Serialization utilities for converting Python objects to various formats.""" + +from .json import serialize_defaults, serialize_json + +__all__ = ["serialize_defaults", "serialize_json"] diff --git a/src/uipath/core/serialization/json.py b/src/uipath/core/serialization/json.py new file mode 100644 index 0000000..0208454 --- /dev/null +++ b/src/uipath/core/serialization/json.py @@ -0,0 +1,150 @@ +"""JSON serialization utilities for converting Python objects to JSON formats.""" + +import json +from dataclasses import asdict, is_dataclass +from datetime import datetime, timezone +from enum import Enum +from typing import Any, cast +from zoneinfo import ZoneInfo + +from pydantic import BaseModel + + +def serialize_defaults( + obj: Any, +) -> dict[str, Any] | list[Any] | str | int | float | bool | None: + """Convert Python objects to JSON-serializable formats. + + Handles common Python types that are not natively JSON-serializable: + - Pydantic models (v1 and v2) + - Dataclasses + - Enums + - Datetime objects + - Timezone objects + - Named tuples + - Sets and tuples + + This function is designed to be used as the `default` parameter in json.dumps(): + ```python + import json + result = json.dumps(obj, default=serialize_defaults) + ``` + + Or use the convenience function `serialize_json()` which wraps this: + ```python + result = serialize_json(obj) + ``` + + Args: + obj: The object to serialize + + Returns: + A JSON-serializable representation of the object: + - Pydantic models: dict from model_dump() + - Dataclasses: dict from asdict() + - Enums: the enum value (recursively serialized) + - datetime: ISO format string + - timezone/ZoneInfo: timezone name + - sets/tuples: converted to lists + - named tuples: converted to dict + - Primitives (None, bool, int, float, str, list, dict): returned unchanged + - Other types: converted to string with str() + + Examples: + >>> from datetime import datetime + >>> from pydantic import BaseModel + >>> + >>> class User(BaseModel): + ... name: str + ... created_at: datetime + >>> + >>> user = User(name="Alice", created_at=datetime.now()) + >>> import json + >>> json.dumps(user, default=serialize_defaults) + '{"name": "Alice", "created_at": "2024-01-01T12:00:00"}' + >>> # Or use the convenience function + >>> serialize_json(user) + '{"name": "Alice", "created_at": "2024-01-01T12:00:00"}' + """ + # Handle Pydantic BaseModel instances + if hasattr(obj, "model_dump") and not isinstance(obj, type): + return obj.model_dump(exclude_none=True, mode="json") + + # Handle Pydantic model classes - convert to schema representation + if isinstance(obj, type) and issubclass(obj, BaseModel): + return { + "__class__": obj.__name__, + "__module__": obj.__module__, + "schema": obj.model_json_schema(), + } + + # Handle Pydantic v1 models + if hasattr(obj, "dict") and not isinstance(obj, type): + return obj.dict() + + # Handle objects with to_dict method + if hasattr(obj, "to_dict") and not isinstance(obj, type): + return obj.to_dict() + + # Handle dataclasses + if is_dataclass(obj) and not isinstance(obj, type): + return asdict(obj) + + # Handle enums - recursively serialize the value + if isinstance(obj, Enum): + return serialize_defaults(obj.value) + + # Handle sets and tuples + if isinstance(obj, (set, tuple)): + # Check if it's a named tuple (has _asdict method) + if hasattr(obj, "_asdict") and callable( + obj._asdict # pyright: ignore[reportAttributeAccessIssue] + ): + return cast( + dict[str, Any], + obj._asdict(), # pyright: ignore[reportAttributeAccessIssue] + ) + # Convert to list + return list(obj) + + # Handle datetime objects + if isinstance(obj, datetime): + return obj.isoformat() + + # Handle timezone objects + if isinstance(obj, (timezone, ZoneInfo)): + return obj.tzname(None) + + # Allow JSON-serializable primitives to pass through unchanged + if obj is None or isinstance(obj, (bool, int, float, str, list, dict)): + return obj + + # Fallback: convert to string + return str(obj) + + +def serialize_json(obj: Any) -> str: + """Serialize Python object to JSON string. + + This is a convenience function that wraps json.dumps() with serialize_defaults() + as the default handler for non-JSON-serializable types. + + Args: + obj: The object to serialize to JSON + + Returns: + JSON string representation of the object + + Examples: + >>> from datetime import datetime + >>> from pydantic import BaseModel + >>> + >>> class Task(BaseModel): + ... name: str + ... created: datetime + >>> + >>> task = Task(name="Review PR", created=datetime(2024, 1, 15, 10, 30)) + >>> serialize_json(task) + '{"name": "Review PR", "created": "2024-01-15T10:30:00"}' + """ + return json.dumps(obj, default=serialize_defaults) diff --git a/src/uipath/core/tracing/_utils.py b/src/uipath/core/tracing/_utils.py index afe3540..841aa67 100644 --- a/src/uipath/core/tracing/_utils.py +++ b/src/uipath/core/tracing/_utils.py @@ -3,14 +3,11 @@ import inspect import json from collections.abc import Callable -from dataclasses import asdict, is_dataclass -from datetime import datetime, timezone -from enum import Enum -from typing import Any, Mapping, Optional, cast -from zoneinfo import ZoneInfo +from typing import Any, Mapping, Optional from opentelemetry.trace import Span -from pydantic import BaseModel + +from uipath.core.serialization import serialize_json def get_supported_params( @@ -31,64 +28,19 @@ def get_supported_params( return supported -def _simple_serialize_defaults( - obj: Any, -) -> dict[str, Any] | list[Any] | str | int | float | bool | None: - # Handle Pydantic BaseModel instances - if hasattr(obj, "model_dump") and not isinstance(obj, type): - return obj.model_dump(exclude_none=True, mode="json") - - # Handle classes - convert to schema representation - if isinstance(obj, type) and issubclass(obj, BaseModel): - return { - "__class__": obj.__name__, - "__module__": obj.__module__, - "schema": obj.model_json_schema(), - } - if hasattr(obj, "dict") and not isinstance(obj, type): - return obj.dict() - if hasattr(obj, "to_dict") and not isinstance(obj, type): - return obj.to_dict() - - # Handle dataclasses - if is_dataclass(obj) and not isinstance(obj, type): - return asdict(obj) - - # Handle enums - if isinstance(obj, Enum): - return _simple_serialize_defaults(obj.value) - - if isinstance(obj, (set, tuple)): - if hasattr(obj, "_asdict") and callable(obj._asdict): # pyright: ignore[reportAttributeAccessIssue] - return cast(dict[str, Any], obj._asdict()) # pyright: ignore[reportAttributeAccessIssue] - return list(obj) - - if isinstance(obj, datetime): - return obj.isoformat() - - if isinstance(obj, (timezone, ZoneInfo)): - return obj.tzname(None) - - # Allow JSON-serializable primitives to pass through unchanged - if obj is None or isinstance(obj, (bool, int, float, str)): - return obj - - return str(obj) - - def format_args_for_trace_json( signature: inspect.Signature, *args: Any, **kwargs: Any ) -> str: """Return a JSON string of inputs from the function signature.""" result = format_args_for_trace(signature, *args, **kwargs) - return json.dumps(result, default=_simple_serialize_defaults) + return serialize_json(result) def format_object_for_trace_json( input_object: Any, ) -> str: """Return a JSON string of inputs from the function signature.""" - return json.dumps(input_object, default=_simple_serialize_defaults) + return serialize_json(input_object) def format_args_for_trace( diff --git a/tests/serialization/test_json.py b/tests/serialization/test_json.py new file mode 100644 index 0000000..663b292 --- /dev/null +++ b/tests/serialization/test_json.py @@ -0,0 +1,614 @@ +"""Tests for serialization utilities.""" + +import json +from collections import namedtuple +from dataclasses import dataclass +from datetime import datetime, timezone +from enum import Enum +from typing import Any +from zoneinfo import ZoneInfo + +import pytest +from pydantic import BaseModel + +from uipath.core.serialization import serialize_json + + +def _has_tzdata() -> bool: + """Check if timezone data is available.""" + try: + ZoneInfo("America/New_York") + return True + except Exception: + return False + + +class Color(Enum): + """Test enum.""" + + RED = "red" + GREEN = "green" + BLUE = 3 + + +class Priority(Enum): + """Test enum with int values.""" + + LOW = 1 + MEDIUM = 2 + HIGH = 3 + + +class SimpleModel(BaseModel): + """Simple Pydantic v2 model.""" + + name: str + value: int + + +class NestedModel(BaseModel): + """Pydantic model with nested model.""" + + id: str + inner: SimpleModel + items: list[SimpleModel] + + +@dataclass +class SimpleDataclass: + """Simple dataclass for testing.""" + + name: str + count: int + + +@dataclass +class NestedDataclass: + """Dataclass with nested dataclass.""" + + id: str + inner: SimpleDataclass + + +Point = namedtuple("Point", ["x", "y"]) + + +class TestSimpleSerializeDefaults: + """Tests for serialize_defaults and serialize_json functions.""" + + def test_serializes_none(self) -> None: + """Test None serialization via json.dumps.""" + data = {"value": None} + result = serialize_json(data) + parsed = json.loads(result) + assert parsed["value"] is None + + def test_serializes_primitives(self) -> None: + """Test primitive types pass through json.dumps unchanged.""" + data = { + "bool_true": True, + "bool_false": False, + "integer": 42, + "float": 3.14, + "string": "hello", + } + result = serialize_json(data) + parsed = json.loads(result) + assert parsed["bool_true"] is True + assert parsed["bool_false"] is False + assert parsed["integer"] == 42 + assert parsed["float"] == 3.14 + assert parsed["string"] == "hello" + + def test_serializes_pydantic_model(self) -> None: + """Test Pydantic BaseModel serialization via json.dumps.""" + model = SimpleModel(name="test", value=42) + result = serialize_json(model) + parsed = json.loads(result) + assert isinstance(parsed, dict) + assert parsed["name"] == "test" + assert parsed["value"] == 42 + + def test_serializes_nested_pydantic_model(self) -> None: + """Test nested Pydantic models via json.dumps.""" + inner = SimpleModel(name="inner", value=10) + model = NestedModel( + id="123", + inner=inner, + items=[ + SimpleModel(name="item1", value=1), + SimpleModel(name="item2", value=2), + ], + ) + result = serialize_json(model) + parsed = json.loads(result) + assert isinstance(parsed, dict) + assert parsed["id"] == "123" + assert parsed["inner"]["name"] == "inner" + assert len(parsed["items"]) == 2 + assert parsed["items"][0]["name"] == "item1" + assert parsed["items"][1]["value"] == 2 + + def test_serializes_pydantic_model_excludes_none(self) -> None: + """Test Pydantic model with None values excluded via json.dumps.""" + + class OptionalModel(BaseModel): + required: str + optional: str | None = None + + model = OptionalModel(required="value") + result = serialize_json(model) + parsed = json.loads(result) + assert isinstance(parsed, dict) + assert parsed["required"] == "value" + # exclude_none=True should exclude the None field + assert "optional" not in parsed + + def test_serializes_pydantic_model_class(self) -> None: + """Test Pydantic model class (not instance) serialization via json.dumps.""" + data = {"model_class": SimpleModel} + result = serialize_json(data) + parsed = json.loads(result) + assert isinstance(parsed["model_class"], dict) + assert parsed["model_class"]["__class__"] == "SimpleModel" + assert parsed["model_class"]["__module__"] == "test_json" + assert "schema" in parsed["model_class"] + assert isinstance(parsed["model_class"]["schema"], dict) + + def test_serializes_dataclass(self) -> None: + """Test dataclass serialization via json.dumps.""" + obj = SimpleDataclass(name="test", count=5) + result = serialize_json(obj) + parsed = json.loads(result) + assert isinstance(parsed, dict) + assert parsed["name"] == "test" + assert parsed["count"] == 5 + + def test_serializes_nested_dataclass(self) -> None: + """Test nested dataclass serialization via json.dumps.""" + inner = SimpleDataclass(name="inner", count=10) + obj = NestedDataclass(id="123", inner=inner) + result = serialize_json(obj) + parsed = json.loads(result) + assert isinstance(parsed, dict) + assert parsed["id"] == "123" + assert parsed["inner"]["name"] == "inner" + assert parsed["inner"]["count"] == 10 + + def test_serializes_enum_string_value(self) -> None: + """Test enum with string value via json.dumps.""" + data = {"color": Color.RED} + result = serialize_json(data) + parsed = json.loads(result) + assert parsed["color"] == "red" + + def test_serializes_enum_int_value(self) -> None: + """Test enum with int value via json.dumps.""" + data = {"priority": Priority.HIGH} + result = serialize_json(data) + parsed = json.loads(result) + assert parsed["priority"] == 3 + + def test_serializes_enum_mixed_value(self) -> None: + """Test enum with mixed types via json.dumps.""" + data = {"color1": Color.GREEN, "color2": Color.BLUE} + result = serialize_json(data) + parsed = json.loads(result) + assert parsed["color1"] == "green" + assert parsed["color2"] == 3 + + def test_serializes_datetime(self) -> None: + """Test datetime serialization via json.dumps.""" + dt = datetime(2024, 1, 15, 10, 30, 45) + data = {"timestamp": dt} + result = serialize_json(data) + parsed = json.loads(result) + assert isinstance(parsed["timestamp"], str) + assert "2024-01-15" in parsed["timestamp"] + assert "10:30:45" in parsed["timestamp"] + + def test_serializes_datetime_with_timezone(self) -> None: + """Test datetime with timezone via json.dumps.""" + dt = datetime(2024, 1, 15, 10, 30, 45, tzinfo=timezone.utc) + data = {"timestamp": dt} + result = serialize_json(data) + parsed = json.loads(result) + assert isinstance(parsed["timestamp"], str) + assert parsed["timestamp"] == "2024-01-15T10:30:45+00:00" + + def test_serializes_timezone(self) -> None: + """Test timezone object serialization via json.dumps.""" + tz = timezone.utc + data = {"timezone": tz} + result = serialize_json(data) + parsed = json.loads(result) + assert parsed["timezone"] == "UTC" + + @pytest.mark.skipif(not _has_tzdata(), reason="Timezone data not available") + def test_serializes_zoneinfo(self) -> None: + """Test ZoneInfo serialization via json.dumps.""" + tz = ZoneInfo("America/New_York") + data = {"timezone": tz} + result = serialize_json(data) + parsed = json.loads(result) + # ZoneInfo returns timezone name or None + assert isinstance(parsed["timezone"], (str, type(None))) + + def test_serializes_set(self) -> None: + """Test set serialization via json.dumps.""" + obj = {1, 2, 3} + data = {"numbers": obj} + result = serialize_json(data) + parsed = json.loads(result) + assert isinstance(parsed["numbers"], list) + assert set(parsed["numbers"]) == {1, 2, 3} + + def test_serializes_tuple(self) -> None: + """Test tuple serialization via json.dumps.""" + obj = (1, 2, 3) + data = {"numbers": obj} + result = serialize_json(data) + parsed = json.loads(result) + assert isinstance(parsed["numbers"], list) + assert parsed["numbers"] == [1, 2, 3] + + def test_serializes_named_tuple(self) -> None: + """Test named tuple serialization via json.dumps. + + Note: Python's json encoder treats namedtuples as regular tuples, + so they serialize as lists [x, y] rather than dicts {"x": x, "y": y}. + The serialize_defaults function is not called for namedtuples + because they're natively JSON-serializable as tuples. + """ + point = Point(x=10, y=20) + data = {"point": point} + result = serialize_json(data) + parsed = json.loads(result) + # Namedtuples serialize as lists through json.dumps + assert isinstance(parsed["point"], list) + assert parsed["point"] == [10, 20] + + def test_serializes_object_with_to_dict(self) -> None: + """Test object with to_dict method via json.dumps.""" + + class CustomObject: + def to_dict(self) -> dict[str, Any]: + return {"custom": "value"} + + obj = CustomObject() + data = {"obj": obj} + result = serialize_json(data) + parsed = json.loads(result) + assert parsed["obj"] == {"custom": "value"} + + def test_serializes_unknown_object_to_str(self) -> None: + """Test unknown object falls back to str() via json.dumps.""" + + class CustomClass: + def __str__(self) -> str: + return "custom_string" + + obj = CustomClass() + data = {"obj": obj} + result = serialize_json(data) + parsed = json.loads(result) + assert parsed["obj"] == "custom_string" + + def test_with_json_dumps(self) -> None: + """Test integration with json.dumps().""" + + class ComplexObject(BaseModel): + name: str + created_at: datetime + priority: Priority + + obj = ComplexObject( + name="task", + created_at=datetime(2024, 1, 15, 10, 30), + priority=Priority.HIGH, + ) + + # Should not raise TypeError + result = serialize_json(obj) + parsed = json.loads(result) + + assert parsed["name"] == "task" + assert "2024-01-15" in parsed["created_at"] + assert parsed["priority"] == 3 + + def test_with_json_dumps_complex_nested(self) -> None: + """Test with complex nested structure.""" + data = { + "model": SimpleModel(name="test", value=42), + "dataclass": SimpleDataclass(name="dc", count=5), + "enum": Color.RED, + "datetime": datetime(2024, 1, 1), + "set": {1, 2, 3}, + "tuple": (4, 5, 6), + } + + result = serialize_json(data) + parsed = json.loads(result) + + assert parsed["model"]["name"] == "test" + assert parsed["dataclass"]["name"] == "dc" + assert parsed["enum"] == "red" + assert "2024-01-01" in parsed["datetime"] + assert set(parsed["set"]) == {1, 2, 3} + assert parsed["tuple"] == [4, 5, 6] + + def test_with_list_of_pydantic_models(self) -> None: + """Test with list of Pydantic models (common MCP scenario).""" + models = [ + SimpleModel(name="first", value=1), + SimpleModel(name="second", value=2), + SimpleModel(name="third", value=3), + ] + + # This should not raise TypeError + result = serialize_json(models) + parsed = json.loads(result) + + assert len(parsed) == 3 + assert parsed[0]["name"] == "first" + assert parsed[1]["value"] == 2 + + def test_recursive_enum_serialization(self) -> None: + """Test that enum values are recursively serialized via json.dumps.""" + + class NestedEnum(Enum): + """Enum with complex value.""" + + COMPLEX = {"key": "value"} + + data = {"enum": NestedEnum.COMPLEX} + result = serialize_json(data) + parsed = json.loads(result) + # The enum value itself (a dict) should be returned as-is + assert parsed["enum"] == {"key": "value"} + assert isinstance(parsed["enum"], dict) + + def test_dataclass_class_returns_string(self) -> None: + """Test that dataclass class (not instance) falls back to str via json.dumps.""" + data = {"dataclass_class": SimpleDataclass} + result = serialize_json(data) + parsed = json.loads(result) + assert isinstance(parsed["dataclass_class"], str) + assert "SimpleDataclass" in parsed["dataclass_class"] + + def test_empty_collections(self) -> None: + """Test empty collections via json.dumps.""" + data: dict[str, Any] = {"empty_set": set(), "empty_tuple": (), "empty_list": []} + result = serialize_json(data) + parsed = json.loads(result) + assert parsed["empty_set"] == [] + assert parsed["empty_tuple"] == [] + assert parsed["empty_list"] == [] + + def test_with_dict_method(self) -> None: + """Test object with dict() method (Pydantic v1 compatibility) via json.dumps.""" + + class OldStyleModel: + def dict(self) -> dict[str, Any]: + return {"old": "style"} + + obj = OldStyleModel() + data = {"obj": obj} + result = serialize_json(data) + parsed = json.loads(result) + assert parsed["obj"] == {"old": "style"} + + def test_dict_of_pydantic_models(self) -> None: + """Test dictionary containing Pydantic models as values.""" + data = { + "user1": SimpleModel(name="Alice", value=100), + "user2": SimpleModel(name="Bob", value=200), + "user3": SimpleModel(name="Charlie", value=300), + } + + result = serialize_json(data) + parsed = json.loads(result) + + assert isinstance(parsed, dict) + assert parsed["user1"]["name"] == "Alice" + assert parsed["user1"]["value"] == 100 + assert parsed["user2"]["name"] == "Bob" + assert parsed["user2"]["value"] == 200 + assert parsed["user3"]["name"] == "Charlie" + assert parsed["user3"]["value"] == 300 + + def test_dict_of_dataclass_models(self) -> None: + """Test dictionary containing dataclass instances as values.""" + data = { + "item1": SimpleDataclass(name="First", count=1), + "item2": SimpleDataclass(name="Second", count=2), + "item3": SimpleDataclass(name="Third", count=3), + } + + result = serialize_json(data) + parsed = json.loads(result) + + assert isinstance(parsed, dict) + assert parsed["item1"]["name"] == "First" + assert parsed["item1"]["count"] == 1 + assert parsed["item2"]["name"] == "Second" + assert parsed["item2"]["count"] == 2 + assert parsed["item3"]["name"] == "Third" + assert parsed["item3"]["count"] == 3 + + def test_normal_class_fallback_to_str(self) -> None: + """Test normal class (not Pydantic, dataclass, or enum) falls back to str().""" + + class RegularClass: + def __init__(self, value: str) -> None: + self.value = value + + def __str__(self) -> str: + return f"RegularClass({self.value})" + + obj = RegularClass("test_value") + data = {"object": obj, "name": "test"} + json_result = serialize_json(data) + parsed = json.loads(json_result) + assert parsed["object"] == "RegularClass(test_value)" + assert parsed["name"] == "test" + + def test_list_of_dataclass(self) -> None: + """Test list containing dataclass instances.""" + data = [ + SimpleDataclass(name="First", count=1), + SimpleDataclass(name="Second", count=2), + SimpleDataclass(name="Third", count=3), + ] + + result = serialize_json(data) + parsed = json.loads(result) + + assert isinstance(parsed, list) + assert len(parsed) == 3 + assert parsed[0]["name"] == "First" + assert parsed[0]["count"] == 1 + assert parsed[1]["name"] == "Second" + assert parsed[2]["count"] == 3 + + def test_list_of_pydantic_models(self) -> None: + """Test list containing Pydantic model instances.""" + data = [ + SimpleModel(name="Alice", value=100), + SimpleModel(name="Bob", value=200), + SimpleModel(name="Charlie", value=300), + ] + + result = serialize_json(data) + parsed = json.loads(result) + + assert isinstance(parsed, list) + assert len(parsed) == 3 + assert parsed[0]["name"] == "Alice" + assert parsed[0]["value"] == 100 + assert parsed[1]["name"] == "Bob" + assert parsed[2]["value"] == 300 + + def test_list_of_normal_classes(self) -> None: + """Test list containing normal class instances (fallback to str).""" + + class Item: + def __init__(self, id: int, label: str) -> None: + self.id = id + self.label = label + + def __str__(self) -> str: + return f"Item(id={self.id}, label={self.label})" + + data = [ + Item(1, "First"), + Item(2, "Second"), + Item(3, "Third"), + ] + + result = serialize_json(data) + parsed = json.loads(result) + + assert isinstance(parsed, list) + assert len(parsed) == 3 + assert parsed[0] == "Item(id=1, label=First)" + assert parsed[1] == "Item(id=2, label=Second)" + assert parsed[2] == "Item(id=3, label=Third)" + + def test_list_of_mixed_types(self) -> None: + """Test list containing mixed types: Pydantic, dataclass, normal class, primitives.""" + + class CustomItem: + def __init__(self, name: str) -> None: + self.name = name + + def __str__(self) -> str: + return f"CustomItem({self.name})" + + data = [ + SimpleModel(name="pydantic", value=1), + SimpleDataclass(name="dataclass", count=2), + CustomItem("custom"), + "plain_string", + 42, + True, + None, + Color.RED, + ] + + result = serialize_json(data) + parsed = json.loads(result) + + assert isinstance(parsed, list) + assert len(parsed) == 8 + # Pydantic model + assert parsed[0]["name"] == "pydantic" + assert parsed[0]["value"] == 1 + # Dataclass + assert parsed[1]["name"] == "dataclass" + assert parsed[1]["count"] == 2 + # Normal class (str fallback) + assert parsed[2] == "CustomItem(custom)" + # Primitives + assert parsed[3] == "plain_string" + assert parsed[4] == 42 + assert parsed[5] is True + assert parsed[6] is None + # Enum + assert parsed[7] == "red" + + def test_list_of_lists_mixed(self) -> None: + """Test nested lists containing mixed types.""" + + class Widget: + def __init__(self, id: int) -> None: + self.id = id + + def __str__(self) -> str: + return f"Widget({self.id})" + + data = [ + [SimpleModel(name="model1", value=1), SimpleModel(name="model2", value=2)], + [ + SimpleDataclass(name="dc1", count=10), + SimpleDataclass(name="dc2", count=20), + ], + [Widget(1), Widget(2), Widget(3)], + ["string1", "string2"], + [1, 2, 3, 4], + [Color.RED, Color.GREEN, Priority.HIGH], + [True, False, None], + ] + + result = serialize_json(data) + parsed = json.loads(result) + + assert isinstance(parsed, list) + assert len(parsed) == 7 + + # First sublist: Pydantic models + assert len(parsed[0]) == 2 + assert parsed[0][0]["name"] == "model1" + assert parsed[0][1]["value"] == 2 + + # Second sublist: Dataclasses + assert len(parsed[1]) == 2 + assert parsed[1][0]["name"] == "dc1" + assert parsed[1][1]["count"] == 20 + + # Third sublist: Normal classes + assert len(parsed[2]) == 3 + assert parsed[2][0] == "Widget(1)" + assert parsed[2][2] == "Widget(3)" + + # Fourth sublist: Strings + assert parsed[3] == ["string1", "string2"] + + # Fifth sublist: Integers + assert parsed[4] == [1, 2, 3, 4] + + # Sixth sublist: Enums + assert parsed[5] == ["red", "green", 3] + + # Seventh sublist: Booleans and None + assert parsed[6] == [True, False, None] diff --git a/uv.lock b/uv.lock index 855d460..75e1d3c 100644 --- a/uv.lock +++ b/uv.lock @@ -991,7 +991,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.2.1" +version = "0.2.2" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" },