Source code for advanced_alchemy.cache.serializers

"""Serialization utilities for caching SQLAlchemy models."""

from typing import Any, TypeVar

from sqlalchemy import inspect as sa_inspect

from advanced_alchemy._serialization import (
    decode_complex_type,
    decode_json,
    encode_complex_type,
    encode_json,
)

__all__ = (
    "default_deserializer",
    "default_serializer",
)

T = TypeVar("T")

_MODEL_KEY = "__aa_model__"
"""Metadata key for the model class name in serialized data."""

_TABLE_KEY = "__aa_table__"
"""Metadata key for the table name in serialized data."""


[docs] def default_serializer(model: Any) -> bytes: """Serialize a SQLAlchemy model instance to JSON bytes. This function extracts column values from a SQLAlchemy model and serializes them to JSON format. The serialized data includes metadata about the model class for validation during deserialization. Note: Relationships are NOT serialized. Only column values are included. The deserialized object will be a detached instance without relationship data loaded. Args: model: The SQLAlchemy model instance to serialize. Returns: JSON-encoded bytes representation of the model. Example: Serializing a model:: user = User(id=1, name="John", email="john@example.com") data = default_serializer(user) # b'{"__aa_model__": "User", "__aa_table__": "users", "id": 1, ...}' """ mapper = sa_inspect(model.__class__) data: dict[str, Any] = { _MODEL_KEY: model.__class__.__name__, _TABLE_KEY: model.__class__.__tablename__, } for column in mapper.columns: # Skip internal SQLAlchemy sentinel columns (e.g., sa_orm_sentinel) if getattr(column, "_insert_sentinel", False): continue value = getattr(model, column.key) # Encode special types into JSON-friendly marker structures. if (encoded := encode_complex_type(value)) is not None: data[column.key] = encoded else: data[column.key] = value return encode_json(data).encode("utf-8")
[docs] def default_deserializer(data: bytes, model_class: type[T]) -> T: """Deserialize JSON bytes to a SQLAlchemy model instance. Creates a new, detached instance of the model class populated with the serialized column values. The instance is NOT attached to any session and should be treated as a read-only snapshot. Warning: The returned instance is detached and does not have relationships loaded. Accessing lazy-loaded relationships will raise DetachedInstanceError. Use ``session.merge()`` if you need to work with relationships. Args: data: JSON bytes to deserialize. model_class: The SQLAlchemy model class to instantiate. Returns: A new, detached instance of the model class. Raises: ValueError: If the serialized data is for a different model class. Example: Deserializing data:: data = b'{"__aa_model__": "User", "id": 1, "name": "John"}' user = default_deserializer(data, User) # user is a detached User instance """ parsed_raw = decode_json(data) parsed = decode_complex_type(parsed_raw) # Validate model class matches serialized_model = parsed.pop(_MODEL_KEY, None) parsed.pop(_TABLE_KEY, None) # Remove table key, not needed for instantiation if serialized_model and serialized_model != model_class.__name__: msg = f"Cannot deserialize {serialized_model} data as {model_class.__name__}" raise ValueError(msg) # Create detached instance using constructor # This properly initializes SQLAlchemy's ORM state instance: T = model_class(**parsed) return instance