My objective is to create a new field based on another field after a Request
is posted to FastAPI. My attempt was:
import functools
from datetime import date, datetime
from slugify import slugify
from sqlmodel import Field, SQLModel
class ProjectBase(SQLModel):
is_active: bool = Field(default=True, nullable=False)
name: str = Field(..., nullable=False)
publish_date: date = Field(default_factory=datetime.now().date, nullable=False)
# # post_init
repr_name: str = Field(
default_factory=functools.partial(slugify, name, separator='_'),
description="The project represented name, it must contain no whitespace, each word is separated by an underscore and it is slugified using the python-slugify library.",
nullable=False)
I have also tried __post_init__
but I think SQLModel does not have such a mechanism, it belongs to dataclasses
within pydantic
.
My desired output would be something like if a Request
like the below was POST-ed:
request = {
'is_active': true,
'name': 'hello world bye',
'publish_date': '2023-01-01'
}
Then, the following Response
is gotten and inserted into the database:
response = {
'is_active': true,
'name': 'hello world bye',
'repr_name': 'hello_world_bye', # output of slugify(`name`, separator='_')
'publish_date': '2023-01-01'
}
Thankfully sqlmodel.SQLModel
inherits from pydantic.BaseModel
. (And their metaclasses are also related.)
This is yet another job for custom validators. We just need a default sentinel value to check against, if no explicit value is provided by the user. I would just make the field type a union of str
and None
and make None
the default, but then always ensure a str
ends up as the value via the validator.
Here is an example:
from datetime import date, datetime
from typing import Any, Optional
from pydantic import validator
from slugify import slugify
from sqlmodel import Field, SQLModel
class ProjectBase(SQLModel):
name: str = Field(..., nullable=False)
publish_date: date = Field(default_factory=datetime.now().date, nullable=False)
repr_name: Optional[str] = Field(default=None, nullable=False)
@validator("repr_name", always=True)
def slugify_name(cls, v: Optional[str], values: dict[str, Any]) -> str:
if v is None:
return slugify(values["name"], separator="_")
return slugify(v, separator="_")
print(ProjectBase(name="foo bar baz").json(indent=2))
print(ProjectBase(name="spam", repr_name="Eggs and Bacon").json(indent=2))
Output:
{
"name": "foo bar baz",
"publish_date": "2023-02-11",
"repr_name": "foo_bar_baz"
}
{
"name": "spam",
"publish_date": "2023-02-11",
"repr_name": "eggs_and_bacon"
}
The always=True
is important to trigger validation, when no value was explicitly provided.
The order of the fields is also important because the values
dictionary in the validator method will only contain previously validated fields and field validation occurs in the order the fields were defined (see docs link above), so since name
comes before repr_name
, it will be validated and its value therefore present in the dictionary, when repr_name
validation is triggered.
Important: Validation will not work, if the validators belong to a table=True
model. To get around this, define a base model with the fields you need validated and inherit from that in your table=True
model; then parse data through the parent model before instantiating the table model with it.
This bit seems pretty limiting, so I would not rule out the possibility that this will change in the future, at least to a certain extent. But in the current state of SQLModel that is what we have.