Advanced Usage¶
This guide covers advanced features of Serilux.
Class Name Conflict Detection¶
Serilux automatically detects and prevents class name conflicts when using @register_serializable.
This ensures that deserialization always uses the correct class definition and helps catch bugs early.
How It Works:
When you register a class with @register_serializable, Serilux checks if a class with the same name
is already registered. If a different class (not the same object) with the same name exists, a
ValueError is raised with detailed information about the conflict.
Example:
from serilux import Serializable, register_serializable
@register_serializable
class Processor(Serializable):
def __init__(self):
super().__init__()
self.name = ""
self.add_serializable_fields(["name"])
# This will raise ValueError
@register_serializable
class Processor(Serializable): # Different class, same name
def __init__(self):
super().__init__()
self.value = 0 # Different implementation
self.add_serializable_fields(["value"])
Error Message:
The error message provides clear information about the conflict:
ValueError: Class name conflict: 'Processor' is already registered as
<class 'mymodule.Processor'>. Cannot register <class 'mymodule.Processor'>.
Please use a different class name or unregister the existing class first.
Best Practices:
Use Unique Class Names: Always use unique, descriptive class names across your project
Avoid Generic Names: Avoid generic names like
TestClass,MyClass, etc. in production codeUse Module-Specific Prefixes: If you have classes that might conflict, use module-specific prefixes
Re-registration is Safe: Re-registering the same class object is allowed and safe (idempotent)
Re-registering the Same Class:
If you need to re-register the same class (e.g., in tests or during module reload), this is allowed:
@register_serializable
class MyClass(Serializable):
def __init__(self):
super().__init__()
self.field = ""
self.add_serializable_fields(["field"])
# Re-registering the same class is allowed
from serilux.serializable import SerializableRegistry
SerializableRegistry.register_class("MyClass", MyClass) # OK, no error
Nested Objects¶
Serilux automatically handles nested Serializable objects:
@register_serializable
class Address(Serializable):
def __init__(self):
super().__init__()
self.street = ""
self.city = ""
self.add_serializable_fields(["street", "city"])
@register_serializable
class Person(Serializable):
def __init__(self):
super().__init__()
self.name = ""
self.address = None
self.add_serializable_fields(["name", "address"])
Lists and Dictionaries¶
Serilux handles lists and dictionaries containing Serializable objects:
@register_serializable
class Team(Serializable):
def __init__(self):
super().__init__()
self.name = ""
self.members = []
self.add_serializable_fields(["name", "members"])
team = Team()
team.name = "Engineering"
team.members = [person1, person2, person3]
data = team.serialize()
Deeply Nested Container Structures¶
Serilux supports arbitrarily deep nesting of containers (dicts and lists) containing Serializable objects. The serialization and deserialization process recursively handles nested structures at any depth.
Example: Dict -> List -> Dict -> Serializable
@register_serializable
class Person(Serializable):
def __init__(self):
super().__init__()
self._id = None
self.name = ""
self.age = 0
self.add_serializable_fields(["_id", "name", "age"])
@register_serializable
class Team(Serializable):
def __init__(self):
super().__init__()
self._id = None
self.name = ""
# Structure: departments[dept_name][role] = [Person, Person, ...]
self.departments = {}
self.add_serializable_fields(["_id", "name", "departments"])
team = Team()
team._id = "team1"
team.name = "Engineering"
person1 = Person()
person1._id = "p1"
person1.name = "Alice"
person1.age = 30
# Nested structure: dict -> dict -> list -> Serializable
team.departments = {
"backend": {
"senior": [person1],
"junior": [person2]
},
"frontend": {
"senior": [person1]
}
}
# Serialize - automatically handles nested structure
data = team.serialize()
# Deserialize - all nested objects are automatically registered
new_team = Team()
registry = ObjectRegistry()
registry.register(new_team, object_id="team1")
new_team.deserialize(data, registry=registry)
# All nested Person objects are accessible
assert new_team.departments["backend"]["senior"][0].name == "Alice"
Example: List -> Dict -> List -> Serializable
@register_serializable
class Project(Serializable):
def __init__(self):
super().__init__()
self._id = None
self.name = ""
# Structure: projects = [{project_name: {phase: [Person, ...]}}]
self.projects = []
self.add_serializable_fields(["_id", "name", "projects"])
project_manager = Project()
project_manager.projects = [
{
"project_a": {
"phase1": [person1],
"phase2": [person2]
}
},
{
"project_b": {
"phase1": [person1, person2]
}
}
]
# Serialize and deserialize work seamlessly
data = project_manager.serialize()
new_manager = Project()
registry = ObjectRegistry()
registry.register(new_manager, object_id="manager1")
new_manager.deserialize(data, registry=registry)
Key Features:
Recursive Serialization: Automatically serializes Serializable objects at any nesting depth
Automatic Registration: All nested Serializable objects are automatically registered in Phase 1
Method Support: Methods in nested objects work correctly after deserialization
No Depth Limit: Supports arbitrary nesting levels (4+ levels tested)
How It Works:
Serialization: The
_serialize_value()method recursively processes nested dicts and lists, automatically serializing any Serializable objects it encounters.Deserialization Phase 1: A recursive function
find_and_register_serializables()traverses the nested structure, finds all Serializable objects, creates instances, and registers them in the ObjectRegistry before deserialization begins.Deserialization Phase 2: The
deserialize_item()method checks the registry first before creating new objects, ensuring that pre-registered objects are reused and methods can find their owner objects.
Callable Serialization¶
Serilux supports serializing and deserializing callable objects (functions, methods, and lambda expressions). This is useful for storing callbacks, handlers, or conditional logic.
Automatic Callable Handling¶
When you have a callable field in a Serializable object, Serilux automatically serializes it:
@register_serializable
class Processor(Serializable):
def __init__(self):
super().__init__()
self.name = ""
self.handler = None # Will be a function or method
self.add_serializable_fields(["name", "handler"])
def process_data(data):
return data.upper()
processor = Processor()
processor.name = "Uppercase Processor"
processor.handler = process_data # Function is automatically serialized
data = processor.serialize()
# handler is serialized as: {"_type": "callable", "callable_type": "function", ...}
Serializing Functions¶
Module-level functions are automatically serialized:
from serilux import serialize_callable
def my_function(x):
return x * 2
serialized = serialize_callable(my_function)
# Returns: {"_type": "callable", "callable_type": "function", "module": "...", "name": "my_function"}
Serializing Methods¶
Methods are serialized with their object reference:
@register_serializable
class Handler(Serializable):
def __init__(self):
super().__init__()
self._id = "handler1"
self.process = self.process_data # Method reference
self.add_serializable_fields(["process"])
def process_data(self, data):
return data.upper()
handler = Handler()
data = handler.serialize()
# process is serialized with object_id reference
Deserializing Callables¶
When deserializing, you need to provide an ObjectRegistry for methods:
from serilux import deserialize_callable, ObjectRegistry
# For methods, create a registry and register the object
registry = ObjectRegistry()
registry.register(handler, object_id="handler1")
# Deserialize the callable
callable_data = data["process"]
restored_method = deserialize_callable(callable_data, registry=registry)
Lambda Expressions¶
Lambda functions and function bodies can be serialized as expressions:
from serilux import serialize_callable_with_fallback
# Lambda function
condition = lambda x: x.get('priority') == 'high'
serialized = serialize_callable_with_fallback(condition)
# Returns: {"_type": "lambda_expression", "expression": "x.get('priority') == 'high'"}
# Deserialize
from serilux import deserialize_lambda_expression
restored = deserialize_lambda_expression(serialized)
ObjectRegistry¶
The ObjectRegistry is used to find objects by ID during deserialization, especially for methods:
from serilux import ObjectRegistry
registry = ObjectRegistry()
# Register objects
registry.register(obj1, object_id="obj1")
registry.register(obj2, object_id="obj2")
# Find by ID
obj = registry.find_by_id("obj1")
# Find by class and ID
obj = registry.find_by_class_and_id("MyClass", "obj1")
# Register multiple objects
registry.register_many({"obj1": obj1, "obj2": obj2})
Using the Registry¶
You can manually register classes:
from serilux import SerializableRegistry
SerializableRegistry.register_class("MyClass", MyClass)
# Retrieve a class
cls = SerializableRegistry.get_class("MyClass")
Two-Phase Deserialization¶
Serilux uses a two-phase deserialization process for containers (dicts/lists) containing Serializable objects:
Phase 1: Pre-create all Serializable instances and register them in the ObjectRegistry
Phase 2: Deserialize all instances (so callables can reference them)
This ensures that methods can reference their owner objects even when there are circular references.
# This happens automatically when you call deserialize()
obj.deserialize(data, registry=registry)