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_class

You can specify a custom class to use as the base for the Pydantic model’s config. This class should be a subclass of pydantic.BaseConfig.

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:

class MyPydanticConfig(pydantic.BaseConfig):
    extra = 'forbid'

model = create_pydantic_model(
    table=MyTable,
    pydantic_config_class=MyPydanticConfig
)

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")

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_class: Optional[Type[BaseConfig]] = None, **schema_extra_kwargs) 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_class – Config class to use as base for the generated pydantic model. You can create your own subclass of pydantic.BaseConfig and pass it here.

  • schema_extra_kwargs

    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.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.