Rich metadata system for controlling field behavior without modifying class definitions. Supports aliasing, ordering, conditional serialization, validation attachment, and schema constraints.
Define validation constraints and schema information for fields and types.
def schema(
*,
title: Optional[str] = None,
description: Optional[str] = None,
default: Any = Undefined,
examples: Optional[Sequence[Any]] = None,
deprecated: Optional[Deprecated] = None,
# Number constraints
min: Optional[Number] = None,
max: Optional[Number] = None,
exc_min: Optional[Number] = None,
exc_max: Optional[Number] = None,
mult_of: Optional[Number] = None,
# String constraints
format: Optional[str] = None,
media_type: Optional[str] = None,
encoding: Optional[ContentEncoding] = None,
min_len: Optional[int] = None,
max_len: Optional[int] = None,
pattern: Optional[Union[str, Pattern]] = None,
# Array constraints
min_items: Optional[int] = None,
max_items: Optional[int] = None,
unique: Optional[bool] = None,
# Object constraints
min_props: Optional[int] = None,
max_props: Optional[int] = None,
# Extra data
extra: Optional[Extra] = None,
override: bool = False,
) -> Schema:
"""
Create schema metadata with validation constraints and documentation.
Parameters:
- title: Human-readable title for the field/type
- description: Detailed description
- default: Default value factory
- examples: Example values for documentation
- deprecated: Deprecation information
- min/max: Numeric range constraints (inclusive)
- exc_min/exc_max: Numeric range constraints (exclusive)
- mult_of: Multiple-of constraint for numbers
- format: String format specification (e.g., "email", "uri")
- media_type: Media type for content
- encoding: Content encoding specification
- min_len/max_len: String/array length constraints
- pattern: Regex pattern for string validation
- min_items/max_items: Array size constraints
- unique: Array uniqueness constraint
- min_props/max_props: Object property count constraints
- extra: Additional schema data
- override: Override inherited schema metadata
Returns:
Schema metadata object
"""
@dataclass(frozen=True)
class Schema(MetadataMixin):
"""
Schema metadata for fields and types.
Contains validation constraints, documentation, and schema generation options.
"""
title: Optional[str] = None
description: Optional[str] = None
default: Optional[Callable[[], Any]] = None
examples: Optional[Sequence[Any]] = None
format: Optional[str] = None
deprecated: Optional[Deprecated] = None
media_type: Optional[str] = None
encoding: Optional[ContentEncoding] = None
constraints: Optional[Constraints] = None
extra: Optional[Callable[[Dict[str, Any]], None]] = None
override: bool = False
child: Optional["Schema"] = NoneTransform field names during serialization and deserialization without changing the Python field names.
def alias(alias_: str, *, override: bool = True) -> Metadata:
"""
Create field alias metadata.
Parameters:
- alias_: Alternative name for the field in serialized data
- override: Override inherited aliases
Returns:
Alias metadata object
"""
def alias(aliaser: Aliaser) -> Callable[[Cls], Cls]:
"""
Class decorator for applying aliaser function to all fields.
Parameters:
- aliaser: Function that transforms field names
Returns:
Class decorator function
"""Configure union type discrimination based on field values for polymorphic deserialization.
@dataclass(frozen=True, unsafe_hash=False)
class Discriminator(MetadataMixin):
"""
Discriminator configuration for union types.
Enables polymorphic deserialization based on discriminator field values.
"""
alias: str # Field name used for discrimination
mapping: Union[
Mapping[str, AnyType],
Callable[[str, Sequence[AnyType]], Mapping[str, AnyType]]
] = default_discriminator_mapping
override_implicit: bool = True
def get_mapping(self, types: Sequence[AnyType]) -> Mapping[str, AnyType]:
"""Get discriminator value to type mapping."""
def discriminator(alias: str, mapping: Mapping[str, AnyType] = None) -> Discriminator:
"""
Create discriminator metadata for union types.
Parameters:
- alias: Field name to use for discrimination
- mapping: Optional explicit mapping of values to types
Returns:
Discriminator metadata object
"""Control the order of fields in serialization output and schema generation.
def order(__value: int) -> Ordering:
"""
Create ordering metadata with numeric order.
Parameters:
- __value: Numeric order (lower numbers come first)
Returns:
Ordering metadata object
"""
def order(*, after: Any) -> Ordering:
"""
Create ordering metadata to place field after another.
Parameters:
- after: Field to place this field after
Returns:
Ordering metadata object
"""
def order(*, before: Any) -> Ordering:
"""
Create ordering metadata to place field before another.
Parameters:
- before: Field to place this field before
Returns:
Ordering metadata object
"""
@dataclass(frozen=True)
class Ordering(MetadataMixin):
"""
Field ordering metadata.
Controls the order of fields in serialization and schema generation.
"""
order: Optional[int] = None
after: Optional[Any] = None
before: Optional[Any] = NoneConfigure handling of additional properties in objects during serialization and deserialization.
def properties(pattern: Union[str, Pattern, ellipsis] = ...) -> Metadata:
"""
Create properties metadata for additional properties handling.
Parameters:
- pattern: Pattern for additional property names (... = allow all)
Returns:
Properties metadata object
"""
@dataclass(frozen=True)
class PropertiesMetadata(MetadataMixin):
"""
Additional properties configuration.
Controls how extra properties are handled during serialization/deserialization.
"""
pattern: Union[str, Pattern, ellipsis] = ...Metadata for controlling various field behaviors during serialization and deserialization.
# Conversion metadata
@dataclass(frozen=True)
class ConversionMetadata(MetadataMixin):
"""
Conversion-specific metadata for fields.
Attributes:
- deserialization: Custom deserialization conversion
- serialization: Custom serialization conversion
"""
deserialization: Optional["AnyConversion"] = None
serialization: Optional["AnyConversion"] = None
conversion = ConversionMetadata # Convenience factory
# Skip metadata
@dataclass(frozen=True)
class SkipMetadata(MetadataMixin):
"""
Configure field skipping behavior.
Attributes:
- deserialization: Skip during deserialization
- serialization: Skip during serialization
- serialization_default: Skip default values during serialization
- serialization_if: Conditional serialization predicate
"""
deserialization: bool = False
serialization: bool = False
serialization_default: bool = False
serialization_if: Optional[Callable[[Any], Any]] = None
# Predefined skip behaviors
skip = SkipMetadata(deserialization=True, serialization=True) # Skip completely
skip_serialization = SkipMetadata(serialization=True) # Skip only serialization
skip_deserialization = SkipMetadata(deserialization=True) # Skip only deserialization
# Field behavior flags
default_as_set: Metadata # Mark field as set when using default value
fall_back_on_default: Metadata # Use default when deserialization fails
flatten: Metadata # Flatten nested object fields
none_as_undefined: Metadata # Treat None as undefined
post_init: Metadata # Call after __init__
required: Metadata # Mark field as required regardless of defaultAttach validators directly to fields through metadata.
def validators(*validator: Callable) -> ValidatorsMetadata:
"""
Create validators metadata for field-level validation.
Parameters:
- validator: Validation functions to attach to field
Returns:
Validators metadata object
"""
@dataclass(frozen=True)
class ValidatorsMetadata(MetadataMixin):
"""
Field-level validators metadata.
Attributes:
- validators: List of validation functions for the field
"""
validators: Tuple[Callable, ...] = ()Define field dependencies where presence of one field requires others.
def dependent_required(
fields: Mapping[Any, Collection[Any]],
*groups: Collection[Any],
owner: Optional[type] = None,
) -> Callable:
"""
Create dependent field requirements.
Parameters:
- fields: Mapping of field to its required dependencies
- groups: Groups of mutually dependent fields
- owner: Owner class for the dependencies
Returns:
Dependency configuration function
"""Define custom type names for schema generation in different contexts.
def type_name(
ref: Optional[NameOrFactory] = None,
*,
json_schema: Optional[NameOrFactory] = None,
graphql: Optional[NameOrFactory] = None,
) -> TypeNameFactory:
"""
Create type naming metadata for schema generation.
Parameters:
- ref: Default reference name
- json_schema: Specific name for JSON Schema generation
- graphql: Specific name for GraphQL schema generation
Returns:
Type name factory function
"""
class TypeName(NamedTuple):
"""
Type name configuration for different schema contexts.
Attributes:
- json_schema: Name to use in JSON Schema
- graphql: Name to use in GraphQL schema
"""
json_schema: Optional[str] = None
graphql: Optional[str] = Nonefrom dataclasses import dataclass, field
from apischema import schema
@dataclass
class User:
username: str = field(metadata=schema(
min_len=3,
max_len=20,
pattern=r"^[a-zA-Z0-9_]+$",
description="Alphanumeric username between 3-20 characters"
))
email: str = field(metadata=schema(
format="email",
description="Valid email address"
))
age: int = field(metadata=schema(
min=13,
max=120,
description="Age in years"
))
score: float = field(metadata=schema(
min=0.0,
max=100.0,
mult_of=0.1,
description="Score as percentage with one decimal place"
))
# Schema constraints are enforced during deserialization
try:
user = deserialize(User, {
"username": "xy", # Too short
"email": "invalid-email", # Invalid format
"age": 10, # Too young
"score": 150.0 # Too high
})
except ValidationError as err:
for error in err.errors:
print(f"{error['loc']}: {error['err']}")@dataclass
class APIUser:
user_id: int = field(metadata=alias("userId"))
first_name: str = field(metadata=alias("firstName"))
last_name: str = field(metadata=alias("lastName"))
is_active: bool = field(metadata=alias("isActive"))
# JSON uses camelCase, Python uses snake_case
json_data = {
"userId": 123,
"firstName": "John",
"lastName": "Doe",
"isActive": True
}
user = deserialize(APIUser, json_data)
print(user.user_id) # 123
print(user.first_name) # "John"
result = serialize(APIUser, user)
print(result) # {"userId": 123, "firstName": "John", "lastName": "Doe", "isActive": true}from apischema import alias, settings
# Apply camelCase aliasing to entire class
@alias(lambda name: ''.join(word.capitalize() if i > 0 else word
for i, word in enumerate(name.split('_'))))
@dataclass
class CamelCaseUser:
user_id: int
full_name: str
email_address: str
# Or use global settings
settings.camel_case = True # Automatically applies camelCase aliasingfrom typing import Union
from apischema import discriminator
@dataclass
class Dog:
type: str = field(default="dog", metadata=discriminator("type"))
breed: str
good_boy: bool = True
@dataclass
class Cat:
type: str = field(default="cat", metadata=discriminator("type"))
breed: str
lives_remaining: int = 9
Animal = Union[Dog, Cat]
# Discriminator field determines which type to deserialize
dog_data = {"type": "dog", "breed": "Golden Retriever", "good_boy": True}
cat_data = {"type": "cat", "breed": "Siamese", "lives_remaining": 7}
dog = deserialize(Animal, dog_data) # Returns Dog instance
cat = deserialize(Animal, cat_data) # Returns Cat instance
print(type(dog)) # <class 'Dog'>
print(type(cat)) # <class 'Cat'>@dataclass
class OrderedData:
id: int = field(metadata=order(1)) # First
name: str = field(metadata=order(2)) # Second
metadata: dict = field(metadata=order(99)) # Last
created_at: str = field(metadata=order(3)) # Third
# Serialization respects field ordering
data = OrderedData(
metadata={"version": "1.0"},
created_at="2023-01-01",
name="Test",
id=1
)
result = serialize(OrderedData, data)
# Output preserves order: {"id": 1, "name": "Test", "created_at": "2023-01-01", "metadata": {...}}@dataclass
class UserProfile:
username: str
email: str
password_hash: str = field(metadata=skip_serialization) # Never serialize
# Conditionally serialize based on value
admin_notes: str = field(
default="",
metadata=SkipMetadata(
serialization_if=lambda x: len(x.strip()) > 0 # Only if not empty
)
)
user = UserProfile(
username="john",
email="john@example.com",
password_hash="hashed_password",
admin_notes="" # Empty, will be skipped
)
result = serialize(UserProfile, user)
print(result) # {"username": "john", "email": "john@example.com"}
# password_hash and admin_notes are excludeddef validate_positive(value: int) -> int:
if value <= 0:
raise ValueError("Value must be positive")
return value
def validate_email_domain(email: str) -> str:
if not email.endswith("@company.com"):
raise ValueError("Must be company email")
return email
@dataclass
class Employee:
id: int = field(metadata=validators(validate_positive))
email: str = field(metadata=validators(validate_email_domain))
salary: float = field(metadata=validators(validate_positive))
# Field validators are applied during deserialization
try:
employee = deserialize(Employee, {
"id": -1, # Fails positive validation
"email": "user@gmail.com", # Fails domain validation
"salary": 50000
})
except ValidationError as err:
print(err.errors) # Shows validation failures for id and email@dataclass
class Product:
# Combine multiple metadata types
name: str = field(metadata=[
schema(min_len=1, max_len=100, description="Product name"),
alias("productName"),
order(1),
validators(lambda x: x.strip()) # Remove whitespace
])
price: float = field(metadata=[
schema(min=0, description="Price in USD"),
order(2),
validators(validate_positive)
])
# Use factory pattern for multiple metadata
description: str = field(
default="",
metadata=schema(
max_len=1000,
description="Product description",
examples=["High-quality product", "Best in class"]
)
)Additional field behavior control functions for specialized use cases.
def default_as_set() -> Metadata:
"""
Mark field as set when using default value.
Used to ensure that fields with defaults are treated as explicitly set
rather than unset during serialization/deserialization.
"""
def fall_back_on_default() -> Metadata:
"""
Fall back to default value when deserialization fails.
Instead of raising an error, use the field's default value
when deserialization encounters problems.
"""
def flatten() -> Metadata:
"""
Flatten nested object fields into parent object.
Aliases: flattened, merged
Used to merge fields from nested objects into the parent structure.
"""
def none_as_undefined() -> Metadata:
"""
Treat None values as undefined during serialization.
Causes None values to be omitted from serialized output
rather than explicitly serialized as null.
"""
def post_init() -> Metadata:
"""
Mark field for post-initialization processing.
Indicates that field should be processed after object construction
is complete, useful for computed or derived fields.
"""
def required() -> Metadata:
"""
Mark field as required during deserialization.
Ensures that the field must be present in input data,
overriding any default value or optional typing.
"""
def init_var(tp: AnyType) -> Metadata:
"""
Mark field as initialization variable.
Parameters:
- tp: The type of the initialization variable
Similar to dataclass init_var, used for fields that are needed
during initialization but not stored in the final object.
"""
def validators(*validator: Callable) -> Metadata:
"""
Attach validators to a field via metadata.
Parameters:
- validator: One or more validator functions
Used with Annotated types to attach field-specific validators:
Annotated[str, validators(check_email_format)]
"""Metadata = Any # Generic metadata type
MetadataMixin = Any # Base class for metadata objects
Aliaser = Callable[[str], str] # Field name transformation function
NameOrFactory = Union[str, Callable[[], str]] # Name or name factory function
TypeNameFactory = Callable[[AnyType], TypeName] # Type name factory function
Number = Union[int, float] # Numeric type for constraints
Pattern = Any # Compiled regex pattern type
ContentEncoding = str # Content encoding specification
Extra = Callable[[Dict[str, Any]], None] # Extra schema data function
Deprecated = Union[bool, str] # Deprecation flag or message