Skip to main content

Field Customization

Customize individual fields using the field() function as a default value in your __init__ method.

Complete Example

from lodum import lodum, field, json

@lodum
class User:
    def __init__(
        self,
        # Rename 'user_id' to 'id' in the output
        user_id: int = field(rename="id", default=0),
        
        # This field is required
        email: str,
        
        # This field will not be included in the output
        password_hash: str = field(skip_serializing=True, default=""),
        
        # If 'prefs' is missing on decoding, it defaults to an empty dict
        prefs: dict = field(default_factory=dict),
        
        # Add validation to a field
        age: int = field(validate=lambda x: x >= 0, default=0)
    ):
        self.user_id = user_id
        self.email = email
        self.password_hash = password_hash
        self.prefs = prefs
        self.age = age

# Encode a user
user = User(email="[email protected]", user_id=123, password_hash="secret")
print(json.dumps(user))
# {"id": 123, "email": "[email protected]", "prefs": {}, "age": 0}

# Decode a user
user_data = '{"id": 456, "email": "[email protected]"}'
user = json.loads(User, user_data)
print(user.user_id)  # 456
print(user.prefs)    # {}

Field Options Reference

rename
str
Use a different name for the field in serialized output
skip_serializing
bool
Exclude the field from serialized output
default
Any
Default value if the field is missing during deserialization
default_factory
callable
Zero-argument function to create default values (for mutable defaults)
serializer
callable
Custom function to encode the field’s value
deserializer
callable
Custom function to decode the field’s value
validate
callable | list[callable]
Function or list of functions to validate the field during deserialization

Built-in Validators

Lodum includes several built-in validators in the lodum.validators module:
from lodum import lodum, field, json
from lodum.validators import Range, Length, Match, OneOf

@lodum
class Product:
    def __init__(
        self,
        name: str = field(validate=Length(min=3, max=50)),
        price: float = field(validate=Range(min=0)),
        category: str = field(validate=OneOf(["electronics", "books", "clothing"])),
        code: str = field(validate=Match(r"^[A-Z]{2}-\d{4}$"))
    ):
        self.name = name
        self.price = price
        self.category = category
        self.code = code

# Valid product
product = json.loads(
    Product,
    '{"name": "Laptop", "price": 999.99, "category": "electronics", "code": "EL-1234"}'
)

# This will raise DeserializationError
try:
    json.loads(Product, '{"name": "A", "price": -10, "category": "food", "code": "abc"}')
except Exception as e:
    print(e)  # Length validation error

Available Validators

from lodum.validators import Range

# Minimum only
field(validate=Range(min=0))

# Maximum only
field(validate=Range(max=100))

# Both min and max
field(validate=Range(min=18, max=120))
from lodum.validators import Length

# Minimum length
field(validate=Length(min=3))

# Maximum length
field(validate=Length(max=50))

# Both
field(validate=Length(min=3, max=20))
from lodum.validators import Match

# Email pattern
field(validate=Match(r"^[a-z]+@[a-z]+\.[a-z]+$"))

# Product code
field(validate=Match(r"^[A-Z]{2}-\d{4}$"))
from lodum.validators import OneOf

# String options
field(validate=OneOf(["admin", "user", "guest"]))

# Numeric options
field(validate=OneOf([1, 2, 3, 5, 8, 13]))

Custom Validators

Create custom validators as simple functions:
from lodum.exception import DeserializationError

def is_even(v):
    if v % 2 != 0:
        raise DeserializationError("Value must be even")

@lodum
class EvenOnly:
    def __init__(self, val: int = field(validate=is_even)):
        self.val = val

json.loads(EvenOnly, '{"val": 4}')  # Success
json.loads(EvenOnly, '{"val": 5}')  # DeserializationError

Multiple Validators

Apply multiple validators to a single field:
@lodum
class MultiValidated:
    def __init__(self, val: int = field(validate=[Range(min=10), Range(max=20)])):
        self.val = val

json.loads(MultiValidated, '{"val": 15}')  # Success
json.loads(MultiValidated, '{"val": 5}')   # Fails min check
json.loads(MultiValidated, '{"val": 25}')  # Fails max check

Complex Type Support

Lodum supports a wide variety of Python types out of the box.

Enums

from enum import Enum

class UserRole(Enum):
    ADMIN = "admin"
    USER = "user"
    GUEST = "guest"

@lodum
class User:
    def __init__(self, name: str, role: UserRole):
        self.name = name
        self.role = role

user = User(name="Alice", role=UserRole.ADMIN)
json_str = json.dumps(user)
# {"name": "Alice", "role": "admin"}

restored = json.loads(User, json_str)
assert restored.role == UserRole.ADMIN

Datetime Objects

Datetime objects are encoded as ISO 8601 strings:
from datetime import datetime

@lodum
class Event:
    def __init__(self, created_at: datetime, name: str):
        self.created_at = created_at
        self.name = name

event = Event(created_at=datetime(2024, 3, 15, 10, 30), name="Meeting")
json_str = json.dumps(event)
# {"created_at": "2024-03-15T10:30:00", "name": "Meeting"}

Sets

@lodum
class Permissions:
    def __init__(self, roles: set[str]):
        self.roles = roles

perms = Permissions(roles={"read", "write", "delete"})
json_str = json.dumps(perms)
restored = json.loads(Permissions, json_str)
assert isinstance(restored.roles, set)

Optional and Union Types

from typing import Optional, Union

@lodum
class FlexibleData:
    def __init__(
        self,
        optional_field: Optional[int],
        union_field: Union[str, bool, int],
        any_field: Any
    ):
        self.optional_field = optional_field
        self.union_field = union_field
        self.any_field = any_field

data = FlexibleData(
    optional_field=None,
    union_field="hello",
    any_field=[1, 2, {"key": "value"}]
)

Generic Types

from typing import Generic, TypeVar

T = TypeVar("T")

@lodum
class Container(Generic[T]):
    def __init__(self, value: T):
        self.value = value

int_container = Container(value=42)
str_container = Container(value="hello")
list_container = Container(value=[1, 2, 3])

Binary Data Handling

Lodum handles binary data differently based on the format:
@lodum
class BinaryData:
    def __init__(self, data: bytes):
        self.data = data

binary = BinaryData(data=b"\x00\x01\x02\x03")

# Text formats (JSON, TOML) use Base64 encoding
json_output = json.dumps(binary)
# {"data": "AAECAw=="}

# Binary formats (MsgPack, CBOR) use native binary
msgpack_output = msgpack.dumps(binary)
Text-based formats (JSON, TOML) encode binary data as Base64 strings.Binary formats (MsgPack, CBOR, BSON, Pickle) and YAML use native binary representation for efficient storage.

Collection Wrappers

Lodum automatically normalizes standard library collection wrappers:
from collections import deque, defaultdict, Counter, OrderedDict

@lodum
class Collections:
    def __init__(
        self,
        queue: deque,
        counts: Counter,
        defaults: defaultdict,
        ordered: OrderedDict
    ):
        self.queue = queue
        self.counts = counts
        self.defaults = defaults
        self.ordered = ordered
These are automatically converted to/from standard list and dict during serialization.

Custom Serializers and Deserializers

Implement custom encoding/decoding logic for specific fields:
from datetime import datetime

def serialize_timestamp(dt: datetime) -> int:
    return int(dt.timestamp())

def deserialize_timestamp(ts: int) -> datetime:
    return datetime.fromtimestamp(ts)

@lodum
class Event:
    def __init__(
        self,
        timestamp: datetime = field(
            serializer=serialize_timestamp,
            deserializer=deserialize_timestamp
        )
    ):
        self.timestamp = timestamp

Next Steps

Streaming

Learn about O(1) memory streaming for large datasets

Schema Generation

Generate JSON schemas from your data models

Build docs developers (and LLMs) love