Skip to content

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.