Serialization

Piccolo uses Pydantic internally to serialize and deserialize data.

Using create_pydantic_model you can easily create Pydantic models for your application.


create_pydantic_model

Using create_pydantic_model we can easily create a Pydantic model from a Piccolo Table.

Using this example schema:

from piccolo.columns import ForeignKey, Integer, Varchar
from piccolo.table import Table

class Manager(Table):
    name = Varchar()

class Band(Table):
    name = Varchar(length=100)
    manager = ForeignKey(Manager)
    popularity = Integer()

Creating a Pydantic model is as simple as:

from piccolo.utils.pydantic import create_pydantic_model

BandModel = create_pydantic_model(Band)

We can then create model instances from data we fetch from the database:

# If using objects:
band = await Band.objects().get(Band.name == 'Pythonistas')
model = BandModel(**band.to_dict())

# If using select:
band = await Band.select().where(Band.name == 'Pythonistas').first()
model = BandModel(**band)

>>> model.name
'Pythonistas'

You have several options for configuring the model, as shown below.

include_columns / exclude_columns

If we want to exclude the popularity column from the Band table:

BandModel = create_pydantic_model(Band, exclude_columns=(Band.popularity,))

Conversely, if you only wanted the popularity column:

BandModel = create_pydantic_model(Band, include_columns=(Band.popularity,))

nested

Another great feature is nested=True. For each ForeignKey in the Piccolo Table, the Pydantic model will contain a sub model for the related table.

For example:

BandModel = create_pydantic_model(Band, nested=True)

If we were to write BandModel by hand instead, it would look like this:

from pydantic import BaseModel

class ManagerModel(BaseModel):
    name: str

class BandModel(BaseModel):
    name: str
    manager: ManagerModel
    popularity: int

But with nested=True we can achieve this with one line of code.

To populate a nested Pydantic model with data from the database:

# If using objects:
band = await Band.objects(Band.manager).get(Band.name == 'Pythonistas')
model = BandModel(**band.to_dict())

# If using select:
band = await Band.select(
    Band.all_columns(),
    Band.manager.all_columns()
).where(
    Band.name == 'Pythonistas'
).first().output(
    nested=True
)
model = BandModel(**band)

>>> model.manager.name
'Guido'

Note

There is a video tutorial on YouTube.

include_default_columns

Sometimes you’ll want to include the Piccolo Table’s primary key column in the generated Pydantic model. For example, in a GET endpoint, we usually want to include the id in the response:

// GET /api/bands/1/
// Response:
{"id": 1, "name": "Pythonistas", "popularity": 1000}

Other times, you won’t want the Pydantic model to include the primary key column. For example, in a POST endpoint, when using a Pydantic model to serialise the payload, we don’t expect the user to pass in an id value:

// POST /api/bands/
// Payload:
{"name": "Pythonistas", "popularity": 1000}

By default the primary key column isn’t included - you can add it using:

BandModel = create_pydantic_model(Band, include_default_columns=True)

pydantic_config

Hint

We used to have a pydantic_config_class argument in Piccolo prior to v1, but it has been replaced with pydantic_config due to changes in Pydantic v2.

You can specify a Pydantic ConfigDict to use as the base for the Pydantic model’s config (see docs).

For example, let’s set the extra parameter to tell pydantic how to treat extra fields (that is, fields that would not otherwise be in the generated model). The allowed values are:

  • 'ignore' (default): silently ignore extra fields

  • 'allow': accept the extra fields and assigns them to the model

  • 'forbid': fail validation if extra fields are present

So if we want to disallow extra fields, we can do:

from pydatic.config import ConfigDict

config: ConfigDict = {
    "extra": "forbid"
}

model = create_pydantic_model(
    table=MyTable,
    pydantic_config=config
)

Required fields

You can specify which fields are required using the required argument of Column. For example:

class Band(Table):
    name = Varchar(required=True)

BandModel = create_pydantic_model(Band)

# Omitting the field raises an error:
>>> BandModel()
ValidationError - name field required

You can override this behaviour using the all_optional argument. An example use case is when you have a model which is used for filtering, then you’ll want all fields to be optional.

class Band(Table):
    name = Varchar(required=True)

BandFilterModel = create_pydantic_model(
    Band,
    all_optional=True,
    model_name='BandFilterModel',
)

# This no longer raises an exception:
>>> BandModel()

Subclassing the model

If the generated model doesn’t perfectly fit your needs, you can subclass it to add additional fields, and to override existing fields.

class Band(Table):
    name = Varchar(required=True)

BandModel = create_pydantic_model(Band)

class CustomBandModel(BandModel):
    genre: str

>>> CustomBandModel(name="Pythonistas", genre="Rock")

Or even simpler still:

class BandModel(create_pydantic_model(Band)):
    genre: str

Avoiding type warnings

Some linters will complain if you use variables in type annotations:

BandModel = create_pydantic_model(Band)


def my_function(band: BandModel):  # Variable not allowed in type expression!
    ...

The fix is really simple:

# We now have a class instead of a variable:
class BandModel(create_pydantic_model(Band)):
    ...


def my_function(band: BandModel):
    ...

Source

piccolo.utils.pydantic.create_pydantic_model(table: Type[Table], nested: Union[bool, Tuple[ForeignKey, ...]] = False, exclude_columns: Tuple[Column, ...] = (), include_columns: Tuple[Column, ...] = (), include_default_columns: bool = False, include_readable: bool = False, all_optional: bool = False, model_name: Optional[str] = None, deserialize_json: bool = False, recursion_depth: int = 0, max_recursion_depth: int = 5, pydantic_config: Optional[ConfigDict] = None, json_schema_extra: Optional[Dict[str, Any]] = None) Type[BaseModel]

Create a Pydantic model representing a table.

Parameters:
  • table – The Piccolo Table you want to create a Pydantic serialiser model for.

  • nested – Whether ForeignKey columns are converted to nested Pydantic models. If False, none are converted. If True, they all are converted. If a tuple of ForeignKey columns is passed in, then only those are converted.

  • exclude_columns – A tuple of Column instances that should be excluded from the Pydantic model. Only specify include_columns or exclude_columns.

  • include_columns – A tuple of Column instances that should be included in the Pydantic model. Only specify include_columns or exclude_columns.

  • include_default_columns – Whether to include columns like id in the serialiser. You will typically include these columns in GET requests, but don’t require them in POST requests.

  • include_readable – Whether to include ‘readable’ columns, which give a string representation of a foreign key.

  • all_optional – If True, all fields are optional. Useful for filters etc.

  • model_name – By default, the classname of the Piccolo Table will be used, but you can override it if you want multiple Pydantic models based off the same Piccolo table.

  • deserialize_json – By default, the values of any Piccolo JSON or JSONB columns are returned as strings. By setting this parameter to True, they will be returned as objects.

  • recursion_depth – Not to be set by the user - used internally to track recursion.

  • max_recursion_depth – If using nested models, this specifies the max amount of recursion.

  • pydantic_config – Allows you to configure some of Pydantic’s behaviour. See the Pydantic docs for more info.

  • json_schema_extra

    This can be used to add additional fields to the schema. This is very useful when using Pydantic’s JSON Schema features. For example:

    >>> my_model = create_pydantic_model(Band, my_extra_field="Hello")
    >>> my_model.model_json_schema()
    {..., "my_extra_field": "Hello"}
    

Returns:

A Pydantic model.

Hint

A good place to see create_pydantic_model in action is PiccoloCRUD, as it uses create_pydantic_model extensively to create Pydantic models from Piccolo tables.


FastAPI template

Piccolo’s FastAPI template uses create_pydantic_model to create serializers.

To create a new FastAPI app using Piccolo, simply use:

piccolo asgi new

See the ASGI docs for more details.