Advanced functionality including field set tracking, global settings configuration, method serialization, recursive type handling, and utility functions for complex use cases.
Track which fields have been explicitly set on objects, useful for partial updates and distinguishing between default values and explicitly set values.
def with_fields_set(cls: Cls) -> Cls:
"""
Class decorator to add field set tracking to dataclass.
Adds tracking of which fields have been explicitly set vs. using defaults.
Parameters:
- cls: Dataclass to add field set tracking to
Returns:
Enhanced class with field set tracking capabilities
"""
def fields_set(obj: Any) -> AbstractSet[str]:
"""
Get the set of field names that have been explicitly set on an object.
Parameters:
- obj: Object to check for set fields
Returns:
Set of field names that were explicitly set (not using defaults)
"""
def is_set(obj: T) -> T:
"""
Return a proxy object for checking if fields are set.
Parameters:
- obj: Object to create proxy for
Returns:
Proxy object where field access returns True if field is set, False otherwise
"""
def set_fields(obj: T, *fields: Any, overwrite: bool = False) -> T:
"""
Mark specific fields as set on an object.
Parameters:
- obj: Object to modify
- fields: Field names to mark as set
- overwrite: Whether to overwrite existing set status
Returns:
Modified object with updated field set status
"""
def unset_fields(obj: T, *fields: Any) -> T:
"""
Mark specific fields as unset on an object.
Parameters:
- obj: Object to modify
- fields: Field names to mark as unset
Returns:
Modified object with updated field set status
"""Include method results in serialization output for computed properties and derived values.
def serialized(
alias: Optional[str] = None,
*,
conversion: Optional[AnyConversion] = None,
error_handler: ErrorHandler = Undefined,
order: Optional[Ordering] = None,
schema: Optional[Schema] = None,
owner: Optional[Type] = None,
) -> Callable:
"""
Decorator to mark methods for inclusion in serialization output.
Parameters:
- alias: Alternative name for the method result in serialized data
- conversion: Custom conversion for the method result
- error_handler: How to handle errors during method execution
- order: Ordering for the method result in output
- schema: Schema metadata for the method result
- owner: Owner class for the method
Returns:
Decorated method that will be included in serialization
"""Configure global defaults for serialization, deserialization, and error handling behavior.
class settings:
"""
Global configuration settings for apischema behavior.
Provides default values for serialization, deserialization, and validation
that can be overridden on a per-operation basis.
"""
# Global settings
additional_properties: bool = False
aliaser: Aliaser = lambda s: s
@property
def camel_case(self) -> bool:
"""Enable automatic camelCase field name transformation."""
@camel_case.setter
def camel_case(self, value: bool) -> None:
"""Set camelCase aliasing on/off."""
class deserialization:
"""Default settings for deserialization operations."""
coerce: bool = False
default_conversion: DefaultConversion = default_deserialization
fall_back_on_default: bool = False
no_copy: bool = True
class serialization:
"""Default settings for serialization operations."""
check_type: bool = False
fall_back_on_any: bool = False
exclude_defaults: bool = False
exclude_none: bool = False
exclude_unset: bool = True
no_copy: bool = True
class errors:
"""Error message templates for various constraint violations."""
# Numeric constraints
minimum: str = "less than {} (minimum)"
maximum: str = "greater than {} (maximum)"
exclusive_minimum: str = "less than or equal to {} (exclusive minimum)"
exclusive_maximum: str = "greater than or equal to {} (exclusive maximum)"
multiple_of: str = "not a multiple of {}"
# String constraints
min_length: str = "shorter than {} characters (minimum length)"
max_length: str = "longer than {} characters (maximum length)"
pattern: str = "does not match pattern '{}'"
format: str = "invalid {} format"
# Array constraints
min_items: str = "fewer than {} items (minimum)"
max_items: str = "more than {} items (maximum)"
unique_items: str = "duplicate items not allowed"
# Object constraints
min_properties: str = "fewer than {} properties (minimum)"
max_properties: str = "more than {} properties (maximum)"
required: str = "required field"
additional_properties: str = "additional properties not allowed"
# Type errors
type_error: str = "{} expected"
value_error: str = "invalid value"Support for dataclass-like initialization variables that don't become instance attributes.
def init_var(tp: AnyType) -> Metadata:
"""
Mark a field as an initialization variable (similar to dataclass InitVar).
The field will be used during object construction but won't be stored
as an instance attribute.
Parameters:
- tp: Type of the initialization variable
Returns:
Metadata marking the field as an init-only variable
"""Core utility functions used throughout the apischema system.
def identity(x: T) -> T:
"""
Identity function that returns its argument unchanged.
Used as a default transformation function in various contexts.
Parameters:
- x: Value to return unchanged
Returns:
The same value passed as input
"""Central type definitions and constant values used throughout apischema.
class UndefinedType:
"""
Type for the Undefined singleton value.
Used to distinguish between None and truly undefined values.
"""
def __bool__(self) -> bool:
return False
def __repr__(self) -> str:
return "Undefined"
Undefined: UndefinedType # Singleton instance representing undefined values
class Unsupported(TypeError):
"""
Exception raised when an operation is not supported for a given type.
Indicates that apischema cannot handle a particular type or operation
in the current context.
"""from dataclasses import dataclass
from apischema.fields import with_fields_set, fields_set, is_set
@with_fields_set
@dataclass
class User:
name: str
email: str
age: int = 25 # Default value
# Create user with some fields set explicitly
user = User(name="John", email="john@example.com") # age uses default
# Check which fields were set
print(fields_set(user)) # {"name", "email"}
print("age" in fields_set(user)) # False - used default
# Check field set status using proxy
is_set_proxy = is_set(user)
print(is_set_proxy.name) # True
print(is_set_proxy.email) # True
print(is_set_proxy.age) # False
# Modify field set status
from apischema.fields import set_fields, unset_fields
user = set_fields(user, "age") # Mark age as explicitly set
print("age" in fields_set(user)) # True
user = unset_fields(user, "name") # Mark name as not set
print("name" in fields_set(user)) # False@with_fields_set
@dataclass
class UserUpdate:
name: Optional[str] = None
email: Optional[str] = None
age: Optional[int] = None
def update_user(user_id: int, updates: UserUpdate) -> None:
"""Update only the fields that were explicitly set."""
set_fields = fields_set(updates)
update_data = {}
if "name" in set_fields:
update_data["name"] = updates.name
if "email" in set_fields:
update_data["email"] = updates.email
if "age" in set_fields:
update_data["age"] = updates.age
# Only update the explicitly set fields in database
db.update_user(user_id, update_data)
# Deserialize partial update - only name is set
update_data = {"name": "Jane"} # email and age not provided
updates = deserialize(UserUpdate, update_data)
print(fields_set(updates)) # {"name"}
update_user(123, updates) # Only updates name fieldfrom apischema.serialization.serialized_methods import serialized
@dataclass
class Rectangle:
width: float
height: float
@serialized # Include in serialization output
def area(self) -> float:
"""Calculate rectangle area."""
return self.width * self.height
@serialized(alias="perimeter") # Use different name in output
def get_perimeter(self) -> float:
"""Calculate rectangle perimeter."""
return 2 * (self.width + self.height)
@serialized(schema=schema(description="Aspect ratio of rectangle"))
def aspect_ratio(self) -> float:
"""Calculate width to height ratio."""
return self.width / self.height if self.height != 0 else float('inf')
rect = Rectangle(width=10, height=5)
result = serialize(Rectangle, rect)
print(result)
# {
# "width": 10,
# "height": 5,
# "area": 50.0,
# "perimeter": 30.0,
# "aspect_ratio": 2.0
# }from apischema import settings
# Configure global defaults
settings.additional_properties = True # Allow extra properties globally
settings.camel_case = True # Use camelCase aliasing by default
# Configure deserialization defaults
settings.deserialization.coerce = True # Enable type coercion
settings.deserialization.fall_back_on_default = True
# Configure serialization defaults
settings.serialization.exclude_defaults = True # Skip default values
settings.serialization.exclude_none = True # Skip None values
# Configure custom error messages
settings.errors.minimum = "value is too small (minimum: {})"
settings.errors.pattern = "value doesn't match required pattern: {}"
@dataclass
class Product:
product_name: str # Will be serialized as "productName" due to camel_case
price: float = 0.0
description: Optional[str] = None
# Global settings are applied automatically
data = {"productName": "Widget", "price": "29.99"} # price as string
product = deserialize(Product, data) # Coercion converts string to float
result = serialize(Product, product) # Excludes price (default) and description (None)
print(result) # {"productName": "Widget"}# Override global settings for specific operations
custom_product = deserialize(
Product,
data,
coerce=False, # Override global coerce setting
additional_properties=False # Override global additional_properties
)
custom_result = serialize(
Product,
product,
exclude_defaults=False, # Include defaults for this serialization
aliaser=lambda s: s.upper() # Custom aliaser overrides camel_case
)from apischema.metadata import init_var
@dataclass
class User:
name: str
email: str
password: str = field(metadata=init_var(str)) # Init-only field
def __post_init__(self, password: str):
"""Process password during initialization."""
self.password_hash = hash_password(password)
# password is not stored as an attribute
# During deserialization, password is used but not stored
user_data = {"name": "John", "email": "john@example.com", "password": "secret"}
user = deserialize(User, user_data)
print(hasattr(user, 'password')) # False - not stored
print(hasattr(user, 'password_hash')) # True - computed in __post_init__
# Serialization doesn't include init-only fields
result = serialize(User, user)
print("password" in result) # Falsefrom apischema.types import Undefined
@dataclass
class Config:
name: str
timeout: int = 30
retries: int = Undefined # Distinguish from None
def process_config(config: Config):
"""Process configuration with undefined handling."""
if config.retries is Undefined:
print("Retries not specified - using adaptive strategy")
elif config.retries is None:
print("Retries explicitly disabled")
else:
print(f"Using {config.retries} retries")
# Different ways to specify retries
config1 = Config(name="test") # retries = Undefined
config2 = deserialize(Config, {"name": "test", "retries": None}) # retries = None
config3 = deserialize(Config, {"name": "test", "retries": 3}) # retries = 3
process_config(config1) # "Retries not specified..."
process_config(config2) # "Retries explicitly disabled"
process_config(config3) # "Using 3 retries"class CustomUnsupportedError(Unsupported):
"""Custom unsupported operation error."""
pass
def handle_complex_type(value: Any) -> Any:
"""Handle complex types with fallback."""
try:
return serialize(type(value), value)
except Unsupported:
# Fall back to string representation
return str(value)
except CustomUnsupportedError:
# Custom handling for specific unsupported types
return {"type": type(value).__name__, "repr": repr(value)}
# Use with various types
result1 = handle_complex_type(datetime.now()) # Uses datetime serializer
result2 = handle_complex_type(lambda x: x) # Falls back to str()
result3 = handle_complex_type(CustomType()) # Custom error handlingT = TypeVar("T") # Generic type variable
Cls = TypeVar("Cls") # Class type variable
ErrorHandler = Union[Callable[[Exception], Any], UndefinedType] # Error handling function
DefaultConversion = Any # Default conversion strategy type
AnyConversion = Union[Conversion, Callable, None] # Any conversion type
Aliaser = Callable[[str], str] # Field name transformation function