Skip to content

Common Arguments

description

A long form description of the field. This value can be used by database adapters to provide additional context to a field.

from protean import Domain
from protean.fields import List, String

domain = Domain(__file__, load_toml=False)


@domain.aggregate
class Building:
    permit = List(
        content_type=String, description="Licences and Approvals", required=True
    )

required

Indicates if the field is required (must have a value). If True, the field is not allowed to be blank. Default is False.

from protean import Domain
from protean.fields import String

domain = Domain(__file__, load_toml=False)


@domain.aggregate
class Person:
    name = String(required=True)

Leaving the field blank or not specifying a value will raise a ValidationError:

In [1]: p = Person()
ERROR: Error during initialization: {'name': ['is required']}
...
ValidationError: {'name': ['is required']}

identifier

If True, the field is an identifier for the entity (a la primary key in RDBMS).

from protean import Domain
from protean.fields import String

domain = Domain(__file__, load_toml=False)


@domain.aggregate
class Person:
    email = String(identifier=True)
    name = String(required=True)

The field is validated to be unique and non-blank:

In [1]: from protean.reflection import declared_fields

In [2]: p = Person(email='john.doe@example.com', name='John Doe')

In [3]: declared_fields(p)
Out[3]: {'email': String(identifier=True), 'name': String(required=True)}

In [4]: p = Person(name='John Doe')
ERROR: Error during initialization: {'email': ['is required']}
...
ValidationError: {'email': ['is required']}

Aggregates and Entities need at least one field to be marked as an identifier. If you don’t specify one, Protean will automatically add a field called id to act as the primary identifier. This means that you don’t need to explicitly set identifier=True on any of your fields unless you want to override the default behavior or the name of the field.

Alternatively, you can use Identifier field type for primary identifier fields.

By default, Protean dynamically generates UUIDs as values of identifier fields unless explicitly provided. You can customize the type of value accepted with identity-strategy config parameter. More details are in Configuration section.

default

The default value for the field if no value is provided.

This can be a value or a callable object. If callable, the function will be called every time a new object is created.

from datetime import datetime, timezone

from protean.domain import Domain
from protean.fields import DateTime, String

publishing = Domain(__name__)


def utc_now():
    return datetime.now(timezone.utc)


@publishing.aggregate
class Post:
    title = String(max_length=50)
    created_at = DateTime(default=utc_now)
In [1]: post = Post(title='Foo')

In [2]: post.to_dict()
Out[2]: 
{'title': 'Foo',
 'created_at': '2024-05-09 00:58:10.781744+00:00',
 'id': '4f6b1fef-bc60-44c2-9ba6-6f844e0d31b0'}

Mutable object defaults

IMPORTANT: The default cannot be a mutable object (list, set, dict, entity instance, etc.), because the reference to the same object would be used as the default in all instances. Instead, wrap the desired default in a callable.

For example, to specify a default list for List field, use a function:

from protean.domain import Domain
from protean.fields import List, String

domain = Domain(__name__)


def standard_topics():
    return ["Music", "Cinema", "Politics"]


@domain.aggregate
class Adult:
    name = String(max_length=255)
    topics = List(default=standard_topics)

Initializing an Adult aggregate will populate the defaults correctly:

In [1]: adult = Adult(name="John Doe")

In [2]: adult.to_dict()
Out[2]: 
{'name': 'John Doe',
 'topics': ['Music', 'Cinema', 'Politics'],
 'id': '14381a6f-b62a-4135-a1d7-d50f68e2afba'}

Lambda expressions

You can use lambda expressions to specify an anonymous function:

import random

from protean.domain import Domain
from protean.fields import Integer

domain = Domain(__name__)

dice_sides = [4, 6, 8, 10, 12, 20]


@domain.aggregate
class Dice:
    sides = Integer(default=lambda: random.choice(dice_sides))

    def throw(self):
        return random.randint(1, self.sides)
In [1]: dice = Dice()

In [2]: dice.to_dict()
Out[2]: {'sides': 6, 'id': '0536ade5-f3a4-4e94-8139-8024756659a7'}

In [3]: dice.throw()
Out[3]: 3

This is a great option when you want to pass parameters to a function.

unique

Indicates if the field values must be unique within the repository. If True, this field's value is validated to be unique among all entities.

from protean import Domain
from protean.fields import String

domain = Domain(__file__, load_toml=False)


@domain.aggregate
class Person:
    name = String(required=True)
    email = String(unique=True)

Obviously, this field's integrity is enforced at the database layer when an entity is persisted. If an entity instance specifies a duplicate value in a field marked unique, a ValidationError will be raised:

In [1]: p1 = Person(name='John Doe', email='john.doe@example.com')

In [2]: domain.repository_for(Person).add(p1)
Out[2]: <Person: Person object (id: b2c592d5-bd78-4e1e-a9d1-eea20ab5374a)>

In [3]: p2 = Person(name= 'Jane Doe', email='john.doe@example.com')

In [4]: domain.repository_for(Person).add(p2)
ERROR: Failed saving entity because {'email': ["Person with email 'john.doe@example.com' is already present."]}
...
ValidationError: {'email': ["Person with email 'john.doe@example.com' is already present."]}

We will explore more about persistence in Application Layer guide.

choices

A set of allowed choices for the field value. When supplied as an Enum, the value of the field is validated to be one among the specified options.

from enum import Enum

from protean import Domain
from protean.fields import Integer, String

domain = Domain(__file__, load_toml=False)


class BuildingStatus(Enum):
    WIP = "WIP"
    DONE = "DONE"


@domain.aggregate
class Building:
    name = String(max_length=50)
    floors = Integer()
    status = String(choices=BuildingStatus)

The choices are enforced when the field is initialized or updated:

In [1]: building = Building(name="Atlantis", floors=3, status="WIP")

In [2]: building.to_dict()
Out[2]: 
{'name': 'Atlantis',
 'floors': 3,
 'status': 'WIP',
 'id': 'c803c763-32d7-403f-b432-8835a258430e'}

In [3]: building.status = "COMPLETED"
ERROR: Error during initialization: {'status': ["Value `'COMPLETED'` is not a valid choice. Must be among ['WIP', 'DONE']"]}
...
ValidationError: {'status': ["Value `'COMPLETED'` is not a valid choice. Must be among ['WIP', 'DONE']"]}

referenced_as

The name of the field as referenced in the database or external systems. Defaults to the field's name.

from protean import Domain
from protean.fields import String

domain = Domain(__file__, load_toml=False)


@domain.aggregate
class Person:
    email = String(unique=True)
    name = String(referenced_as="fullname", required=True)

Protean will now persist the value under fullname instead of name.

In [1]: from protean.reflection import declared_fields, attributes

In [2]: declared_fields(Person)
Out[2]: 
{'email': String(),
 'name': String(required=True, referenced_as='fullname'),
 'id': Auto(identifier=True)}

In [3]: attributes(Person)
Out[3]: 
{'_version': Integer(default=-1),
 'email': String(),
 'fullname': String(required=True, referenced_as='fullname'),
 'id': Auto(identifier=True)}

validators

Additional validators to apply to the field value.

Validators are callable Class instances that are invoked whenever a field's value is changed. Protean's String field, for example, has two default validators: MinLengthValidator and MaxLenghtValidator classes associated with min_length and max_length attributes.

from typing import Any

from protean import Domain
from protean.exceptions import ValidationError
from protean.fields import String

domain = Domain(__file__, load_toml=False)


class EmailDomainValidator:
    def __init__(self, domain="example.com"):
        self.domain = domain
        self.message = f"Email does not belong to {self.domain}"

    def __call__(self, value: str) -> Any:
        if not value.endswith(self.domain):
            raise ValidationError(self.message)


@domain.aggregate
class Employee:
    email = String(identifier=True, validators=[EmailDomainValidator("mydomain.com")])

If the value fails to satisfy the validation, a ValidationError will be thrown with the custom error message.

In [1]: e = Employee(email="john@mydomain.com")

In [2]: e.to_dict()
Out[2]: {'email': 'john@mydomain.com'}

In [3]: e2 = Employee(email="john@otherdomain.com")
ERROR: Error during initialization: {'email': ['Email does not belong to mydomain.com']}
...
ValidationError: {'email': ['Email does not belong to mydomain.com']}

error_messages

Custom error messages for different kinds of errors. If supplied, the default messages that the field will raise will be overridden. Default error message keys that apply to all field types are required, invalid, unique, and invalid_choice. Each field may have additional error message keys as detailed in their documentation.

from protean import Domain
from protean.fields import Integer

domain = Domain(__file__, load_toml=False)


@domain.aggregate
class Building:
    doors = Integer(
        required=True, error_messages={"required": "Every building needs some!"}
    )

Now the custom message will be available in ValidationError:

In [1]: Building()
ERROR: Error during initialization: {'doors': ['Every building needs doors.']}
...
ValidationError: {'doors': ['Every building needs some!']}