JSON (de)serialization, GraphQL and JSON schema generation using Python typing.
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Extensible conversion system for custom serialization and deserialization logic. Register converters for any types including third-party libraries, ORM models, and custom transformations.
Register custom deserialization converters that transform data during the deserialization process.
def deserializer(
deserializer: Optional[Deserializer] = None,
*,
lazy: Optional[Callable[[], Union[Converter, Conversion]]] = None,
target: Optional[Type] = None,
) -> Callable:
"""
Register a deserialization converter.
Parameters:
- deserializer: Function that converts data to target type
- lazy: Lazy-loaded converter function (for avoiding import cycles)
- target: Target type for the converter (inferred if not provided)
Returns:
Decorator function for registration
"""Register custom serialization converters that transform objects during the serialization process.
def serializer(
serializer: Optional[Serializer] = None,
*,
lazy: Optional[Callable[[], Union[Converter, Conversion]]] = None,
source: Optional[Type] = None,
) -> Callable:
"""
Register a serialization converter.
Parameters:
- serializer: Function that converts object to serializable data
- lazy: Lazy-loaded converter function (for avoiding import cycles)
- source: Source type for the converter (inferred if not provided)
Returns:
Decorator function for registration
"""Classes for configuring conversion behavior with detailed options and lazy loading.
@dataclass(frozen=True)
class Conversion:
"""
Conversion configuration with converter function and type information.
Attributes:
- converter: Function that performs the conversion
- source: Source type (for serialization converters)
- target: Target type (for deserialization converters)
- inherited: Whether conversion applies to subclasses
"""
converter: Callable
source: Optional[AnyType] = None
target: Optional[AnyType] = None
inherited: Optional[bool] = None
@dataclass(frozen=True)
class LazyConversion:
"""
Lazy-loaded conversion configuration.
Attributes:
- func: Function that returns Conversion or Converter when called
"""
func: Callable[[], Union[Conversion, Callable]]Built-in utility functions for common conversion patterns.
def catch_value_error(func: Func) -> Func:
"""
Decorator that catches ValueError and re-raises as ValidationError.
Parameters:
- func: Function to wrap
Returns:
Wrapped function that converts ValueError to ValidationError
"""
def as_str(cls: Cls) -> Cls:
"""
Register string conversion for enum classes.
Automatically registers serialization to string (enum.value or enum.name)
and deserialization from string.
Parameters:
- cls: Enum class to register converters for
Returns:
Enum class (for use as decorator)
"""
def as_names(cls: EnumCls, aliaser: Callable[[str], str] = lambda s: s) -> EnumCls:
"""
Register name-based conversion for enum classes with optional aliasing.
Parameters:
- cls: Enum class to register converters for
- aliaser: Function to transform enum names (e.g., snake_case to camelCase)
Returns:
Enum class (for use as decorator)
"""from datetime import datetime
from typing import NewType
from apischema import deserializer, serializer
# Custom type for timestamps
Timestamp = NewType("Timestamp", int)
@deserializer
def deserialize_timestamp(timestamp: int) -> datetime:
"""Convert Unix timestamp to datetime."""
return datetime.fromtimestamp(timestamp)
@serializer
def serialize_timestamp(dt: datetime) -> int:
"""Convert datetime to Unix timestamp."""
return int(dt.timestamp())
# Now datetime objects can be serialized/deserialized as timestamps
@dataclass
class Event:
name: str
created_at: datetime
data = {"name": "Meeting", "created_at": 1672531200} # Unix timestamp
event = deserialize(Event, data)
print(event.created_at) # datetime(2023, 1, 1, 0, 0)
result = serialize(Event, event)
print(result) # {"name": "Meeting", "created_at": 1672531200}from enum import Enum
from apischema.conversions import as_str, as_names
@as_str
class Status(Enum):
PENDING = "pending"
APPROVED = "approved"
REJECTED = "rejected"
@as_names # Use enum names instead of values
class Priority(Enum):
LOW = 1
MEDIUM = 2
HIGH = 3
@dataclass
class Task:
title: str
status: Status
priority: Priority
# Serialization uses enum values/names
data = {"title": "Review", "status": "approved", "priority": "HIGH"}
task = deserialize(Task, data)
print(task.status) # Status.APPROVED
print(task.priority) # Priority.HIGH
result = serialize(Task, task)
print(result) # {"title": "Review", "status": "approved", "priority": "HIGH"}from decimal import Decimal
@deserializer
def deserialize_decimal(value: str) -> Decimal:
"""Convert string to Decimal for precise arithmetic."""
return Decimal(value)
@serializer
def serialize_decimal(value: Decimal) -> str:
"""Convert Decimal to string representation."""
return str(value)
@dataclass
class Price:
amount: Decimal
currency: str
# Decimal values are handled as strings in JSON
data = {"amount": "99.99", "currency": "USD"}
price = deserialize(Price, data)
print(type(price.amount)) # <class 'decimal.Decimal'>
result = serialize(Price, price)
print(result) # {"amount": "99.99", "currency": "USD"}import uuid
from dataclasses import dataclass
@deserializer
def deserialize_uuid(value: str) -> uuid.UUID:
"""Convert string to UUID object."""
return uuid.UUID(value)
@serializer
def serialize_uuid(value: uuid.UUID) -> str:
"""Convert UUID to string representation."""
return str(value)
@dataclass
class User:
id: uuid.UUID
name: str
# UUIDs are handled as strings in JSON
data = {"id": "123e4567-e89b-12d3-a456-426614174000", "name": "John"}
user = deserialize(User, data)
print(type(user.id)) # <class 'uuid.UUID'># Example with SQLAlchemy models
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class UserModel(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String)
@deserializer
def deserialize_user_model(data: dict) -> UserModel:
"""Convert dict to SQLAlchemy model instance."""
return UserModel(**data)
@serializer
def serialize_user_model(user: UserModel) -> dict:
"""Convert SQLAlchemy model to dict."""
return {
"id": user.id,
"name": user.name,
"email": user.email
}
# Now UserModel can be used in apischema serialization
@dataclass
class UserResponse:
user: UserModel
metadata: dict
data = {
"user": {"id": 1, "name": "John", "email": "john@example.com"},
"metadata": {"source": "database"}
}
response = deserialize(UserResponse, data)
print(type(response.user)) # <class 'UserModel'># Avoid import cycles with lazy loading
@deserializer(lazy=lambda: datetime_converter)
def lazy_datetime_deserializer():
"""Lazy-loaded datetime converter."""
pass
def datetime_converter(timestamp: int) -> datetime:
"""Actual conversion function loaded lazily."""
return datetime.fromtimestamp(timestamp)from apischema.conversions import catch_value_error
@catch_value_error
@deserializer
def deserialize_positive_int(value: str) -> int:
"""Convert string to positive integer."""
result = int(value)
if result <= 0:
raise ValueError("Value must be positive")
return result
# ValueError is automatically converted to ValidationError
try:
deserialize(int, "-5") # Uses the converter
except ValidationError as err:
print(err.errors) # Structured error with location infofrom typing import Union
@deserializer
def deserialize_flexible_number(value: Union[str, int, float]) -> float:
"""Convert various number formats to float."""
if isinstance(value, str):
if value.endswith('%'):
return float(value[:-1]) / 100
return float(value)
return float(value)
@dataclass
class Statistics:
growth_rate: float # Can accept "15.5%", "0.155", 15.5, etc.
data = {"growth_rate": "15.5%"}
stats = deserialize(Statistics, data)
print(stats.growth_rate) # 0.155# Register different converters for different contexts
@serializer
def serialize_datetime_iso(dt: datetime) -> str:
"""Serialize datetime as ISO string."""
return dt.isoformat()
@serializer
def serialize_datetime_timestamp(dt: datetime) -> int:
"""Serialize datetime as Unix timestamp."""
return int(dt.timestamp())
# Choose converter at call time
event_data = serialize(Event, event, conversion=serialize_datetime_iso)
timestamp_data = serialize(Event, event, conversion=serialize_datetime_timestamp)Functions for managing and clearing registered conversions.
def reset_deserializers(cls: Type) -> None:
"""
Clear all registered deserializers for a given type.
Parameters:
- cls: The type to clear deserializers for
"""
def reset_serializer(cls: Type) -> None:
"""
Clear all registered serializers for a given type.
Parameters:
- cls: The type to clear serializers for
"""Converter = Callable[[Any], Any] # Basic conversion functionInstall with Tessl CLI
npx tessl i tessl/pypi-apischema