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: 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: str | None = None, deserialize_json: bool = False, recursion_depth: int = 0, max_recursion_depth: int = 5, pydantic_config: ConfigDict | None = None, json_schema_extra: Dict[str, Any] | None = 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. IfFalse
, none are converted. IfTrue
, they all are converted. If a tuple ofForeignKey
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 specifyinclude_columns
orexclude_columns
.include_columns – A tuple of
Column
instances that should be included in the Pydantic model. Only specifyinclude_columns
orexclude_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
orJSONB
columns are returned as strings. By setting this parameter toTrue
, 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.