SQLAlchemy integration with the marshmallow (de)serialization library
—
Specialized marshmallow fields for handling SQLAlchemy relationships with automatic serialization and deserialization of related model instances, supporting both single relationships and collections.
Field for representing SQLAlchemy relationships, handling serialization of related model instances and deserialization back to instances with session-aware loading.
class Related(fields.Field):
"""Field for SQLAlchemy relationships.
Handles serialization/deserialization of related model instances.
Must be attached to a Schema with a SQLAlchemy model.
"""
def __init__(
self,
columns: list[str] | str | None = None,
column: str | None = None, # Deprecated, use columns
**kwargs,
):
"""Initialize Related field.
Parameters:
- columns: Column names on related model for serialization.
If None, uses primary key(s) of related model.
- column: (Deprecated) Single column name, use columns instead.
- **kwargs: Additional field arguments
"""
@property
def model(self) -> type[DeclarativeMeta] | None:
"""The SQLAlchemy model of the parent schema."""
@property
def related_model(self) -> type[DeclarativeMeta]:
"""The SQLAlchemy model of the related object."""
@property
def related_keys(self) -> list[MapperProperty]:
"""Properties used for serialization/deserialization."""
@property
def session(self) -> Session:
"""SQLAlchemy session from parent schema."""
@property
def transient(self) -> bool:
"""Whether parent schema loads transient instances."""List field that extends marshmallow's List field with special handling for collections of related objects in SQLAlchemy relationships.
class RelatedList(fields.List):
"""List field for one-to-many and many-to-many relationships.
Extends marshmallow List field with proper handling for
SQLAlchemy relationship collections.
"""
def get_value(self, obj, attr, accessor=None):
"""Get value with special handling for relationship collections."""Nested field that automatically inherits session and transient state from the parent schema for proper relationship handling.
class Nested(fields.Nested):
"""Nested field that inherits session from parent schema.
Ensures nested schemas have access to the same SQLAlchemy
session for proper relationship loading.
"""
def _deserialize(self, *args, **kwargs):
"""Deserialize with session inheritance."""def get_primary_keys(model: type[DeclarativeMeta]) -> list[MapperProperty]:
"""Get primary key properties for a SQLAlchemy model.
Parameters:
- model: SQLAlchemy model class
Returns:
List of primary key mapper properties
"""
def ensure_list(value: Any) -> list:
"""Ensure value is a list.
Converts iterables to lists, wraps non-iterables in lists.
"""from marshmallow_sqlalchemy import SQLAlchemyAutoSchema, Related
from mymodels import Author, Book
class BookSchema(SQLAlchemyAutoSchema):
class Meta:
model = Book
load_instance = True
include_relationships = True
# Related field automatically created for relationships
# when include_relationships=True
author = Related() # Uses primary key of Author model
class AuthorSchema(SQLAlchemyAutoSchema):
class Meta:
model = Author
load_instance = True
# Manual Related field with custom columns
books = Related(columns=["id", "title"])# Serialization - converts model instances to dictionaries
book = session.get(Book, 1)
schema = BookSchema()
serialized = schema.dump(book)
# Result: {
# "id": 1,
# "title": "Python Guide",
# "author": {"id": 5, "name": "John Doe"} # Related field serialized
# }
# For multiple columns, returns dict
author_with_email = Related(columns=["id", "email"])
# Serializes to: {"id": 5, "email": "john@example.com"}
# For single column (primary key), returns scalar value
author_id_only = Related(columns=["id"])
# Serializes to: 5# Deserialization - converts dictionaries back to model instances
book_data = {
"title": "New Book",
"author": {"id": 5} # Reference to existing author
}
schema = BookSchema()
book_instance = schema.load(book_data, session=session)
# Loads existing Author with id=5 from database
# Creates new Book instance with that author
# Scalar value deserialization (for single primary key)
book_data_scalar = {
"title": "Another Book",
"author": 5 # Direct primary key value
}
book_instance = schema.load(book_data_scalar, session=session)# Load related objects as new instances (not from database)
schema = BookSchema()
book_data = {
"title": "New Book",
"author": {"name": "Jane Doe", "email": "jane@example.com"}
}
# Transient mode - creates new Author instance
book_instance = schema.load(book_data, session=session, transient=True)
# book_instance.author is a new Author instance, not loaded from DBfrom marshmallow_sqlalchemy import RelatedList
class AuthorSchema(SQLAlchemyAutoSchema):
class Meta:
model = Author
load_instance = True
# Automatically created when include_relationships=True
books = RelatedList(Related(columns=["id", "title"]))
# Serialization of collections
author = session.get(Author, 1)
schema = AuthorSchema()
serialized = schema.dump(author)
# Result: {
# "id": 1,
# "name": "John Doe",
# "books": [
# {"id": 1, "title": "First Book"},
# {"id": 2, "title": "Second Book"}
# ]
# }
# Deserialization of collections
author_data = {
"name": "New Author",
"books": [
{"id": 1}, # Load existing book
{"title": "Brand New Book"} # Create new book
]
}
author_instance = schema.load(author_data, session=session)from marshmallow_sqlalchemy import Nested
class BookDetailSchema(SQLAlchemyAutoSchema):
class Meta:
model = Book
load_instance = True
class AuthorDetailSchema(SQLAlchemyAutoSchema):
class Meta:
model = Author
load_instance = True
# Use nested schema for detailed book information
books = Nested(BookDetailSchema, many=True)
# Session automatically inherited by nested schema
schema = AuthorDetailSchema()
author_data = {
"name": "Author Name",
"books": [
{"title": "Book Title", "isbn": "978-1234567890"}
]
}
author_instance = schema.load(author_data, session=session)
# Nested BookDetailSchema automatically gets the sessionclass BookSchema(SQLAlchemyAutoSchema):
class Meta:
model = Book
load_instance = True
# Use specific columns for serialization
author = Related(columns=["id", "name", "email"])
# Use single column (returns scalar)
category_id = Related(columns=["id"])
# Use all columns (default behavior)
publisher = Related() # Uses primary key columns
# Multiple primary key handling
class CompositeKeyModel(Base):
__tablename__ = "composite"
key1 = sa.Column(sa.Integer, primary_key=True)
key2 = sa.Column(sa.String, primary_key=True)
class ParentSchema(SQLAlchemyAutoSchema):
class Meta:
model = Parent
load_instance = True
# Automatically handles multiple primary keys
composite_ref = Related()
# Serializes to: {"key1": 1, "key2": "abc"}
# Deserializes from: {"key1": 1, "key2": "abc"}# Related field validation errors
try:
schema = BookSchema()
result = schema.load({
"title": "Test Book",
"author": "invalid_value" # Should be dict or primary key
}, session=session)
except ValidationError as e:
# e.messages contains field-specific errors
print(e.messages["author"])
# ["Could not deserialize related value 'invalid_value'; expected a dictionary with keys ['id']"]
# Missing related object
try:
result = schema.load({
"title": "Test Book",
"author": {"id": 999} # Non-existent author
}, session=session)
# Creates new Author instance with id=999 if not found in DB
except ValidationError as e:
# Handle validation errors
pass# Related fields work with SQLAlchemy association proxies
class User(Base):
__tablename__ = "users"
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String)
# Association proxy
role_names = association_proxy("user_roles", "role_name")
class UserSchema(SQLAlchemyAutoSchema):
class Meta:
model = User
load_instance = True
include_relationships = True
# Related field handles association proxy relationships
role_names = Related() # Automatically detected and handledInstall with Tessl CLI
npx tessl i tessl/pypi-marshmallow-sqlalchemy