Marshalls in Edgy¶
Marshalls in Edgy provide a powerful mechanism for serializing data and adding extra layers of customization. They allow you to augment Edgy models with additional information that might not be directly accessible during serialization.
Essentially, marshalls facilitate adding validations on top of existing models and customizing the serialization process, including restricting which fields are serialized. While not primarily designed for direct database interaction, marshalls offer an interface to perform such operations if needed, through the save()
method.
Marshall Class¶
The Marshall
class is the base class that must be subclassed when creating a marshall. It's where you define extra fields and specify which fields to serialize.
from edgy.core.marshalls import Marshall
When declaring a Marshall
, you must define a ConfigMarshall and then add any extra fields you want.
Here's a basic example of how to use a marshall:
from typing import Any, Dict
from pydantic import BaseModel
from tests.settings import DATABASE_URL
import edgy
from edgy.core.marshalls import Marshall, fields
from edgy.core.marshalls.config import ConfigMarshall
from edgy.testclient import DatabaseTestClient as Database
database = Database(url=DATABASE_URL)
registry = edgy.Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=100, null=False)
email: str = edgy.EmailField(max_length=100, null=False)
language: str = edgy.CharField(max_length=200, null=True)
description: str = edgy.TextField(max_length=5000, null=True)
class Meta:
registry = registry
@property
def display_name(self) -> str:
return f"Diplay name: {self.name}"
class UserExtra(BaseModel):
address: str
post_code: str
class UserMarshall(Marshall):
marshall_config: ConfigMarshall = ConfigMarshall(model=User, fields=["name", "email"])
details: fields.MarshallMethodField = fields.MarshallMethodField(field_type=str)
extra: fields.MarshallMethodField = fields.MarshallMethodField(field_type=Dict[str, Any])
def get_details(self, instance) -> str:
return instance.display_name
def get_extra(self, instance: edgy.Model) -> Dict[str, Any]:
extra = UserExtra(address="123 street", post_code="90210")
return extra.model_dump()
Let's break this down step by step.
The Marshall
has a marshall_config
attribute that must be declared, specifying the model
and fields
.
The fields
list contains the names of the model fields that should be serialized directly from the model.
The extra
and details
fields are marshall-specific fields, meaning they are not directly from the model but are included in the serialization. You can find more details about these Fields later in this document.
Once the marshall is defined, you can use it like this:
data = {"name": "Edgy", "email": "edgy@example.com"}
marshall = UserMarshall(**data)
marshall.model_dump()
The result will be:
{
"name": "Edgy",
"email": "edgy@example.com",
"details": "Diplay name: Edgy",
"extra": {"address": "123 street", "post_code": "90210"},
}
As you can see, Marshall
is also a Pydantic model, allowing you to leverage its full potential.
There are more operations and customizations you can perform with marshalls, particularly regarding fields, which are covered in the following sections.
ConfigMarshall¶
To work with marshalls, you need to declare a marshall_config
, which is a typed dictionary containing the following keys:
- model: The Edgy model associated with the marshall, or a string
dotted.path
pointing to the model. - fields: A list of strings representing the fields to include in the marshall's serialization.
- exclude: A list of strings representing the fields to exclude from the marshall's serialization.
Warning
You can only declare either fields
or exclude
, but not both. The model
is mandatory, or an exception will be raised.
from tests.settings import DATABASE_URL
import edgy
from edgy.core.marshalls import Marshall
from edgy.core.marshalls.config import ConfigMarshall
from edgy.testclient import DatabaseTestClient as Database
database = Database(url=DATABASE_URL)
registry = edgy.Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=100, null=False)
email: str = edgy.EmailField(max_length=100, null=False)
language: str = edgy.CharField(max_length=200, null=True)
description: str = edgy.TextField(max_length=5000, null=True)
class Meta:
registry = registry
class UserMarshall(Marshall):
marshall_config: ConfigMarshall = ConfigMarshall(
model=User,
fields=["name", "email"],
)
from tests.settings import DATABASE_URL
import edgy
from edgy.core.marshalls import Marshall
from edgy.core.marshalls.config import ConfigMarshall
from edgy.testclient import DatabaseTestClient as Database
database = Database(url=DATABASE_URL)
registry = edgy.Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=100, null=False)
email: str = edgy.EmailField(max_length=100, null=False)
language: str = edgy.CharField(max_length=200, null=True)
description: str = edgy.TextField(max_length=5000, null=True)
class Meta:
registry = registry
class UserMarshall(Marshall):
marshall_config: ConfigMarshall = ConfigMarshall(
model=User,
exclude=["language"],
)
The fields
list also supports the use of __all__
, which includes all fields declared in your Edgy model.
Example:
class CustomMarshall(Marshall):
marshall_config: ConfigMarshall = ConfigMarshall(model=User, fields=['__all__'])
Fields¶
This is where things get interesting. When declaring a Marshall
and adding extra fields to the serialization, you can use two types of fields:
- MarshallField: Used to reference a model field, a Python
property
defined in the Edgy model, or a function. - MarshallMethodField: Used to reference a function defined within the marshall, not the model.
To use these fields, import them:
from edgy.core.marshalls import fields
All fields have a mandatory attribute field_type
, which specifies the Python type used by Pydantic for validation.
Example:
class CustomMarshall(Marshall):
marshall_config: ConfigMarshall = ConfigMarshall(model="myapp.models.MyModel", fields=['__all__'])
details: fields.MarshallMethodField = fields.MarshallMethodField(field_type=Dict[str, Any])
age: fields.MarshallField = fields.MarshallField(int, source="age")
MarshallField¶
This is the most common field type used in marshalls.
Parameters¶
- field_type: The Python type used by Pydantic for data validation.
- source: The source of the field, which can be a model field, a property, or a function.
All values passed in the source must come from the Edgy Model.
Example:
from tests.settings import DATABASE_URL
import edgy
from edgy.core.marshalls import Marshall, fields
from edgy.core.marshalls.config import ConfigMarshall
from edgy.testclient import DatabaseTestClient as Database
database = Database(url=DATABASE_URL)
registry = edgy.Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=100, null=False)
email: str = edgy.EmailField(max_length=100, null=False)
language: str = edgy.CharField(max_length=200, null=True)
description: str = edgy.TextField(max_length=5000, null=True)
class Meta:
registry = registry
@property
def age(self) -> int:
return 1
def details(self) -> str: ...
class UserMarshall(Marshall):
marshall_config: ConfigMarshall = ConfigMarshall(
model=User,
exclude=["language"],
)
name: fields.MarshallField = fields.MarshallField(
field_type=str,
source="name",
)
age: fields.MarshallField = fields.MarshallField(
field_type=int,
source="age",
)
details: fields.MarshallField = fields.MarshallField(
field_type=str,
source="details",
)
MarshallMethodField¶
This field type is used to retrieve extra information provided by the Marshall
itself.
When declaring a MarshallMethodField
, you must define a function named get_
followed by the field name.
Edgy automatically injects an instance of the Edgy model declared in marshall_config
into this function. This instance is not persisted in the database unless you explicitly save it. Therefore, the primary_key
will not be available until then, but other object attributes and operations are.
Parameters¶
- field_type: The Python type used by Pydantic for data validation.
Example:
from typing import Any, Dict
from pydantic import BaseModel
from tests.settings import DATABASE_URL
import edgy
from edgy.core.marshalls import Marshall, fields
from edgy.core.marshalls.config import ConfigMarshall
from edgy.testclient import DatabaseTestClient as Database
database = Database(url=DATABASE_URL)
registry = edgy.Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=100, null=False)
email: str = edgy.EmailField(max_length=100, null=False)
language: str = edgy.CharField(max_length=200, null=True)
description: str = edgy.TextField(max_length=5000, null=True)
class Meta:
registry = registry
@property
def display_name(self) -> str:
return f"Diplay name: {self.name}"
class UserExtra(BaseModel):
address: str
post_code: str
class UserMarshall(Marshall):
marshall_config: ConfigMarshall = ConfigMarshall(
model=User,
exclude=["language"],
)
display_name: fields.MarshallMethodField = fields.MarshallMethodField(field_type=str)
data: fields.MarshallMethodField = fields.MarshallMethodField(field_type=Dict[str, Any])
def get_display_name(self, instance: edgy.Model) -> str:
return instance.display_name()
def get_data(self, instance: edgy.Model) -> Dict[str, Any]:
extra = UserExtra(address="123 street", post_code="90210")
return extra.model_dump()
Including Additional Context¶
In some cases, you might need to provide extra context to a marshall. You can do this by passing a context
argument when instantiating the marshall.
Example:
class UserMarshall(Marshall):
marshall_config: ConfigMarshall = ConfigMarshall(model=User, fields=["name", "email"],)
additional_context: fields.MarshallMethodField = fields.MarshallMethodField(field_type=dict[str, Any])
def get_additional_context(self, instance: edgy.Model) -> dict[str, Any]:
return self.context
data = {"name": "Edgy", "email": "edgy@example.com"}
marshall = UserMarshall(**data, context={"foo": "bar"})
marshall.model_dump()
Result:
{
"name": "Edgy",
"email": "edgy@example.com",
"additional_context": {"foo": "bar"}
}
save()
Method¶
Since Marshall
is a Pydantic base model, similar to Edgy models, you can persist data directly using the marshall.
Edgy provides a save()
method for marshalls that mirrors the save()
method of Edgy models.
Example¶
Using the UserMarshall
from the previous example:
from typing import Any, Dict
from pydantic import BaseModel
from tests.settings import DATABASE_URL
import edgy
from edgy.core.marshalls import Marshall, fields
from edgy.core.marshalls.config import ConfigMarshall
from edgy.testclient import DatabaseTestClient as Database
database = Database(url=DATABASE_URL)
registry = edgy.Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=100, null=False)
email: str = edgy.EmailField(max_length=100, null=False)
language: str = edgy.CharField(max_length=200, null=True)
description: str = edgy.TextField(max_length=5000, null=True)
class Meta:
registry = registry
@property
def display_name(self) -> str:
return f"Diplay name: {self.name}"
class UserExtra(BaseModel):
address: str
post_code: str
class UserMarshall(Marshall):
marshall_config: ConfigMarshall = ConfigMarshall(
model=User,
exclude=["language"],
)
display_name: fields.MarshallMethodField = fields.MarshallMethodField(field_type=str)
data: fields.MarshallMethodField = fields.MarshallMethodField(field_type=Dict[str, Any])
def get_display_name(self, instance: edgy.Model) -> str:
return instance.display_name()
def get_data(self, instance: edgy.Model) -> Dict[str, Any]:
extra = UserExtra(address="123 street", post_code="90210")
return extra.model_dump()
To create and save a User
instance:
data = {
"name": "Edgy",
"email": "edgy@example.com",
"language": "EN",
"description": "Nice marshall"
}
marshall = UserMarshall(**data)
await marshall.save()
The marshall intelligently distinguishes between model fields and marshall-specific fields and persists the model fields.
Extra Considerations¶
Creating marshalls is straightforward, but keep these points in mind:
Model Fields with null=False
¶
When declaring ConfigMarshall
fields
, you must select at least the mandatory fields (null=False
), or a MarshallFieldDefinitionError
will be raised.
This prevents errors during model creation.
Model Validators¶
Model validators (using @model_validator
and @field_validator
) work as expected. You can use them to validate model fields during instance creation.