Custom Types
Crous provides a registry system for custom type serialization. You can register serializers for any Python class and decoders to reconstruct them. Tags are auto-assigned starting from 100.
Registering a Serializer
Use register_serializer(type, serializer_fn, tag=None) to register a serializer function for a Python type. The serializer receives the object and must return a serializable value (dict, list, string, etc.).
import crous
from datetime import datetime
# Define serializer function
def serialize_datetime(dt):
return {
"year": dt.year,
"month": dt.month,
"day": dt.day,
"hour": dt.hour,
"minute": dt.minute,
"second": dt.second,
}
# Register it
crous.register_serializer(datetime, serialize_datetime)
# Now datetime objects serialize automatically!
data = {"created": datetime(2024, 1, 15, 10, 30, 0)}
binary = crous.dumps(data)Registering a Decoder
Use register_decoder(tag, decoder_fn) to register a decoder that reconstructs the custom type from the serialized data.
import crous
from datetime import datetime
# Define decoder function
def decode_datetime(data):
return datetime(
data["year"], data["month"], data["day"],
data["hour"], data["minute"], data["second"]
)
# Register decoder for the same tag
# Tags are auto-assigned starting at 100
crous.register_decoder(100, decode_datetime)
# Full round-trip!
data = {"created": datetime(2024, 1, 15, 10, 30, 0)}
binary = crous.dumps(data)
result = crous.loads(binary)
assert isinstance(result["created"], datetime) # ✓
assert result["created"] == data["created"] # ✓Tag Auto-Assignment
register_serializergets tag 100, the second gets 101, and so on. You can also specify a tag explicitly.Complete Example: Multiple Custom Types
import crous
from datetime import datetime, date
from decimal import Decimal
from uuid import UUID
from pathlib import Path
# --- Serializers ---
crous.register_serializer(datetime, lambda dt: dt.isoformat())
crous.register_serializer(date, lambda d: d.isoformat())
crous.register_serializer(Decimal, lambda d: str(d))
crous.register_serializer(UUID, lambda u: str(u))
crous.register_serializer(Path, lambda p: str(p))
# --- Decoders ---
crous.register_decoder(100, lambda s: datetime.fromisoformat(s))
crous.register_decoder(101, lambda s: date.fromisoformat(s))
crous.register_decoder(102, lambda s: Decimal(s))
crous.register_decoder(103, lambda s: UUID(s))
crous.register_decoder(104, lambda s: Path(s))
# Use it!
data = {
"timestamp": datetime.now(),
"birthday": date(1990, 5, 15),
"price": Decimal("29.99"),
"id": UUID("12345678-1234-5678-1234-567812345678"),
"config": Path("/etc/config.json"),
}
binary = crous.dumps(data)
result = crous.loads(binary)
# All types preserved!
assert isinstance(result["timestamp"], datetime)
assert isinstance(result["birthday"], date)
assert isinstance(result["price"], Decimal)
assert isinstance(result["id"], UUID)
assert isinstance(result["config"], Path)MRO-Aware Lookup
The serializer registry uses Python's Method Resolution Order (MRO) for type lookup. If no exact serializer is registered for a type, Crous walks the MRO chain to find the closest registered parent.
import crous
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
# Register serializer for the base class
crous.register_serializer(Animal, lambda a: {"name": a.name})
# Dog inherits the Animal serializer via MRO!
dog = Dog("Rex", "Labrador")
binary = crous.dumps(dog) # Uses Animal's serializerUnregistering
Remove serializers and decoders when they're no longer needed:
# Remove serializer
crous.unregister_serializer(datetime)
# Remove decoder
crous.unregister_decoder(100)Thread Safety
The custom serializer/decoder registry is protected by a thread lock. Multiple threads can safely:
- Register/unregister serializers concurrently
- Call
dumps/loadsconcurrently - Mix registration and serialization across threads
Thread Safe
PyThread_type_lock before accessing the global serializer/decoder dictionaries. The core encode/decode functions are stateless and can run fully in parallel.How It Works Internally
When encoding, Crous follows this flow for each object:
- Check if the type is a built-in type (None, bool, int, float, str, bytes, list, tuple, dict, set, frozenset)
- If not, check the custom serializer registry (with MRO fallback)
- If found, call the serializer, convert the result to a
crous_value, wrap it in aTAGGEDvalue with the assigned tag - If not found, call the
defaultfunction (if provided) - If no default, raise
CrousEncodeError
When decoding, for each TAGGED value:
- Tag 90 → reconstruct as
set - Tag 91 → reconstruct as
frozenset - Other tags → look up in custom decoder registry
- If decoder found, call it with the inner value
- If not found, return the inner value as-is