Skip to content

Migration Guide

This guide helps you migrate from other popular Python serialization libraries to lodum, and documents changes between lodum versions.

Migrating from previous Lodum versions

Exception Standardization (v0.2.0)

In versions prior to v0.2.0, the YAML and Pickle loaders raised TypeError when a field's type did not match the expected type during deserialization. This has been standardized to lodum.exception.DeserializationError to match the behavior of other formats like JSON and MsgPack.

Before:

try:
    lodum.yaml.loads(MyClass, bad_data)
except TypeError:
    # handle error

After:

from lodum.exception import DeserializationError

try:
    lodum.yaml.loads(MyClass, bad_data)
except DeserializationError:
    # handle error

Centralized Schema API (v0.2.0)

Starting with v0.2.0, the primary way to generate a JSON Schema from a @lodum class is through the top-level lodum.schema() function. Format-specific schema functions (like lodum.msgpack.schema()) have been removed from binary formats to reduce API surface area and architectural redundancy.

Before:

from lodum import msgpack
s = msgpack.schema(MyClass)

After:

import lodum
s = lodum.schema(MyClass)
Note: json.schema() and yaml.schema() remain available as secondary entry points for convenience.


Key Differences

lodum is inspired by Rust's serde framework. Its primary differences from other Python libraries are:

  1. Format Agnostic: lodum separates the definition of your data structure (using @lodum) from the data format (JSON, YAML, TOML, MsgPack, etc.).
  2. Bytecode Compilation: lodum generates specialized Python bytecode for your classes at runtime. This provides performance comparable to hand-written code while remaining pure Python.
  3. __init__-Centric: lodum uses your class's __init__ method and its type hints as the source of truth for the data structure. This ensures that your objects are always instantiated through their standard constructor.

Migrating from Pydantic

Pydantic is a popular library that uses BaseModel and class attributes to define data structures.

Class Definition

Pydantic:

from pydantic import BaseModel

class User(BaseModel):
    id: int
    username: str
    email: str | None = None

lodum:

from lodum import lodum
from typing import Optional

@lodum
class User:
    def __init__(self, id: int, username: str, email: Optional[str] = None):
        self.id = id
        self.username = username
        self.email = email
Note: You can also use @dataclass with @lodum.

Serialization

Pydantic:

user_dict = user.model_dump()
user_json = user.model_dump_json()

lodum:

from lodum import json

# lodum doesn't have a generic 'model_dump', but you can dump to any format
user_json = json.dumps(user)
# If you just want a dict:
from lodum.internal import dump
from lodum.core import BaseDumper
user_dict = dump(user, BaseDumper())

Deserialization

Pydantic:

user = User.model_validate(data_dict)
user = User.model_validate_json(data_json)

lodum:

from lodum import json

user = json.loads(User, data_json)
# From a dict:
from lodum.internal import load
from lodum.json import JSONLoader # or any other loader
user = load(User, JSONLoader(data_dict))

Field Customization

Pydantic:

from pydantic import BaseModel, Field

class User(BaseModel):
    user_id: int = Field(alias="id")
    password: str = Field(exclude=True)

lodum:

from lodum import lodum, field

@lodum
class User:
    def __init__(
        self,
        user_id: int = field(rename="id"),
        password: str = field(skip_serializing=True)
    ):
        self.user_id = user_id
        self.password = password


Migrating from Marshmallow

Marshmallow uses separate Schema classes to define how data is serialized and deserialized.

Definition and Usage

Marshmallow:

from marshmallow import Schema, fields, post_load

class User:
    def __init__(self, id, name):
        self.id = id
        self.name = name

class UserSchema(Schema):
    id = fields.Int(data_key="user_id")
    name = fields.Str()

    @post_load
    def make_user(self, data, **kwargs):
        return User(**data)

schema = UserSchema()
result = schema.load({"user_id": 1, "name": "Alice"})

lodum:

from lodum import lodum, field, json

@lodum
class User:
    def __init__(self, id: int = field(rename="user_id"), name: str = ""):
        self.id = id
        self.name = name

user = json.loads(User, '{"user_id": 1, "name": "Alice"}')
lodum eliminates the need for a separate Schema class and the @post_load boilerplate.


Migrating from Dataclasses (with mashumaro/dacite)

If you are already using dataclasses with a library like mashumaro, the transition to lodum is very smooth.

mashumaro:

from dataclasses import dataclass
from mashumaro import DataClassJSONMixin

@dataclass
class Point(DataClassJSONMixin):
    x: int
    y: int

lodum:

from dataclasses import dataclass
from lodum import lodum, json

@lodum
@dataclass
class Point:
    x: int
    y: int

# Usage
json_str = json.dumps(Point(1, 2))
p = json.loads(Point, json_str)

lodum offers a similar performance profile to mashumaro through bytecode generation, but provides a more unified interface for multiple binary and text formats out of the box.


Migrating from Attrs and Cattrs

attrs is an alternative to dataclasses, and cattrs handles the conversion to/from structured data.

Attrs/Cattrs:

import attr
import cattr

@attr.s
class User:
    id = attr.ib(type=int)
    name = attr.ib(type=str)

user = cattr.structure({"id": 1, "name": "Alice"}, User)

lodum:

from lodum import lodum, json

@lodum
class User:
    def __init__(self, id: int, name: str):
        self.id = id
        self.name = name

user = json.loads(User, '{"id": 1, "name": "Alice"}')
While cattrs is very flexible, lodum provides a more integrated experience with direct support for various wire formats.