Marshalls¶
Saffier marshalls are model-backed serializers and data transfer objects.
They provide:
- controlled output via
model_dump() - Python-native input validation for marshall-specific fields
- computed and sourced fields
- partial update workflows
- a
save()bridge back into Saffier models
Unlike Edgy, Saffier marshalls are not Pydantic models. They are implemented directly on top of Saffier’s field and model system, which keeps the subsystem Python-native and avoids introducing Pydantic as a framework dependency.
Imports¶
Use any of the following:
import saffier
from saffier import ConfigMarshall, Marshall, MarshallField, MarshallMethodField
from saffier import marshalls
from saffier.core.marshalls import ConfigMarshall, Marshall
The public saffier.marshalls namespace mirrors the core marshall API:
from saffier import marshalls
marshalls.Marshall
marshalls.MarshallField
marshalls.MarshallMethodField
Basic Example¶
from typing import ClassVar
import saffier
class User(saffier.Model):
name = saffier.CharField(max_length=100)
email = saffier.EmailField(max_length=100, null=True)
class UserMarshall(saffier.Marshall):
marshall_config: ClassVar[saffier.ConfigMarshall] = saffier.ConfigMarshall(
model=User,
fields=["name", "email"],
)
display_name = saffier.MarshallField(str, source="name")
details = saffier.MarshallMethodField(str)
def get_details(self, instance: User) -> str:
return f"Display name: {instance.name}"
payload = UserMarshall(name="Saffier", email="saffier@ravyn.dev")
payload.model_dump()
Result:
{
"name": "Saffier",
"email": "saffier@ravyn.dev",
"display_name": "Saffier",
"details": "Display name: Saffier"
}
marshall_config¶
Every marshall must define marshall_config.
Supported keys:
model: a Saffier model class or dotted import stringfields: included model field namesexclude: excluded model field namesprimary_key_read_only: mark selected primary keys as read-onlyexclude_autoincrement: remove autoincrement primary keys from the marshallexclude_read_only: remove read-only model fields from the marshall
Rules:
- declare
fieldsorexclude, not both modelis mandatory- if
marshall_configis annotated, useClassVar[...]
Example:
class UserMarshall(saffier.Marshall):
marshall_config: ClassVar[saffier.ConfigMarshall] = saffier.ConfigMarshall(
model=User,
fields=["__all__"],
exclude_autoincrement=True,
)
"__all__" includes all selected model fields in the marshall.
Marshall Fields¶
Saffier supports two marshall-specific field types.
MarshallField¶
Use this when the value should come from:
- a model attribute
- a model property
- a model method with no arguments
- a marshall-local control field
class UserMarshall(saffier.Marshall):
marshall_config: ClassVar[saffier.ConfigMarshall] = saffier.ConfigMarshall(
model=User,
fields=["name"],
)
upper_name = saffier.MarshallField(str, source="upper_name")
Parameters:
field_type: expected Python typesource: alternate attribute/property/method name on the model instanceallow_null: allowNonedefault: static or callable defaultexclude: keep the field on the marshall but remove it frommodel_dump()
exclude=True is the Saffier-native way to declare marshall-local control fields:
class UserMarshall(saffier.Marshall):
marshall_config: ClassVar[saffier.ConfigMarshall] = saffier.ConfigMarshall(
model=User,
fields=["name"],
)
shall_save = saffier.MarshallField(bool, default=False, exclude=True)
MarshallMethodField¶
Use this when the value should come from logic defined on the marshall itself.
class UserMarshall(saffier.Marshall):
marshall_config: ClassVar[saffier.ConfigMarshall] = saffier.ConfigMarshall(
model=User,
fields=["name"],
)
details = saffier.MarshallMethodField(str)
def get_details(self, instance: User) -> str:
return f"User: {instance.name}"
Rules:
- define
get_<field_name>() - the method receives the current model instance
- async getters are supported
Context¶
Marshalls accept an optional context dictionary.
class UserMarshall(saffier.Marshall):
marshall_config: ClassVar[saffier.ConfigMarshall] = saffier.ConfigMarshall(
model=User,
fields=["name"],
)
extra_context = saffier.MarshallMethodField(dict[str, str])
def get_extra_context(self, instance: User) -> dict[str, str]:
return self.context
payload = UserMarshall(name="Saffier", context={"source": "admin"})
payload.model_dump()
Partial Marshalls¶
You can declare a marshall with only part of the model fields and attach the instance later.
class EmailUpdateMarshall(saffier.Marshall):
marshall_config: ClassVar[saffier.ConfigMarshall] = saffier.ConfigMarshall(
model=User,
fields=["email"],
)
payload = EmailUpdateMarshall(email="new@ravyn.dev")
payload.instance = await User.query.get(pk=1)
await payload.save()
If required model fields are missing and no instance is attached, accessing instance or calling
save() raises a runtime error.
Saving¶
await marshall.save() persists the associated model.
Creation:
payload = UserMarshall(name="Saffier", email="saffier@ravyn.dev")
await payload.save()
Update:
user = await User.query.get(pk=1)
payload = UserMarshall(instance=user)
payload.name = "Updated"
await payload.save()
Behavior:
- if the marshall was built from raw values, Saffier creates a model instance and saves it
- if the marshall was built from an existing instance, Saffier updates that instance
- autoincrement primary keys are synchronized back into the marshall after save
Dumping And Schema Output¶
Use model_dump() to serialize the marshall:
payload.model_dump()
payload.model_dump(exclude_none=True)
payload.model_dump(exclude_unset=True)
Use model_json_schema() for a lightweight JSON-schema-style description of the current marshall
surface:
UserMarshall.model_json_schema()
This is intentionally simpler than Pydantic’s schema system. It is designed for inspection, tooling, and admin-style form generation, not for full Pydantic compatibility.
Relationship Guidance¶
Marshalls are strongest for scalar model data and computed output.
For relationships, especially nested foreign keys and many-to-many data, prefer explicit marshall fields instead of relying on implicit object serialization. This keeps output predictable and matches Saffier’s Python-native design.
Error Cases¶
Common configuration errors raise MarshallFieldDefinitionError:
- missing
marshall_config - using both
fieldsandexclude - omitting both
fieldsandexclude - forgetting
ClassVaron annotatedmarshall_config - declaring a
MarshallMethodFieldwithoutget_<name>()