Skip to content

Casts ​

Databases store a fairly small set of types, but your application usually wants richer ones: booleans instead of 1 and 0, dicts instead of JSON strings, dates instead of text. Casts bridge that gap. A cast converts a column value when you read it from the database and again when you write it back. The type annotation on a model field picks the cast, so most of the time you don't have to do anything beyond writing the annotation. You can also declare a field explicitly with Field[T]().

Built-in Casts ​

Type annotationCastExample DB value → Python
intInteger"1" → 1
strString1 → "1"
floatFloat"3.14" → 3.14
boolBoolean1 → True, 0 → False
dictJSON'{"key":"val"}' → {"key": "val"}
listJSON'["a","b"]' → ["a", "b"]
datetimeDateTime"2024-01-01 08:00:00" → datetime.datetime
dateDateTime"2024-01-01" → datetime.date
CarbonDateTime"2024-01-01 00:00:00" → Carbon
timeTime"09:00:00" → datetime.time
timedeltaTimeDelta3600.0 → datetime.timedelta(seconds=3600)

To use them, annotate the model fields. No other configuration is needed:

python
from fastapi_startkit.masoniteorm import Model

class User(Model):
    id: int
    name: str
    is_admin: bool
    score: float
    preferences: dict   # stored as JSON text
    tags: list          # stored as JSON text

When you read a model, the values come back as the annotated type:

python
user = await User.first()

isinstance(user.id, int)          # True
isinstance(user.is_admin, bool)   # True
isinstance(user.preferences, dict) # True

Writing works the same way in reverse. Pass plain Python values, and there's no need for json.dumps:

python
await User.create({
    "name": "Joe",
    "is_admin": True,
    "preferences": {"theme": "dark", "language": "en"},
    "tags": ["admin", "editor"],
})

Date & Time Casts ​

datetime and date ​

A field annotated with datetime or date comes back as a pendulum DateTime, which is a datetime subclass. If you want the field typed as Carbon, along with its helpers, annotate it with Carbon instead.

python
from datetime import datetime, date
from fastapi_startkit.masoniteorm import Model

class User(Model):
    email_verified_at: datetime   # stored as "YYYY-MM-DD HH:MM:SS"
    date_of_birth: date

When writing, you can pass either a string or a native datetime / date:

python
import datetime

await User.create({
    "email_verified_at": "2024-06-15 12:30:00",
    "date_of_birth": datetime.date(1990, 6, 1),
})
python
user = await User.first()

user.email_verified_at.year    # 2024
user.date_of_birth.month       # 6

Carbon ​

python
from fastapi_startkit.carbon import Carbon

class Article(Model):
    published_at: Carbon

time ​

A time field is stored as an "HH:MM:SS" string and returned as datetime.time:

python
from datetime import time

class User(Model):
    punch_in_time: time
python
user = await User.first()
isinstance(user.punch_in_time, time)   # True
user.punch_in_time.hour                # 9

timedelta ​

A timedelta field is stored as a float of total seconds and returned as datetime.timedelta:

python
from datetime import timedelta

class Session(Model):
    duration: timedelta
python
session = await Session.first()
isinstance(session.duration, timedelta)       # True
session.duration.total_seconds()              # 3600.0

Default values ​

If you'd rather not deal with NULL, use Field(default=...) or Field(default_factory=...) to set the value returned when the column is NULL:

python
from datetime import time
from fastapi_startkit.masoniteorm.models.fields import Field

class User(Model):
    punch_in_time: time = Field(default=time(9, 0, 0))

A NULL in the database now reads as time(9, 0, 0).

Nested Pydantic Models ​

Sometimes a group of values belongs together, like the parts of an address. To store a value object like that in one column, define it as a Pydantic BaseModel and use it as the field type. It's serialized to JSON on write and rebuilt as the Pydantic model on read.

Defining a custom cast ​

python
from typing import Optional
from pydantic import BaseModel

class Address(BaseModel):
    street: Optional[str] = None
    city: Optional[str] = None
    state: Optional[str] = None
    country: Optional[str] = None

Wiring it to a model ​

python
from fastapi_startkit.masoniteorm import Model
from app.casts import Address

class User(Model):
    id: int
    name: str
    address: Address

The older form address: Address = ModelField() still works. ModelField is importable from fastapi_startkit.masoniteorm, but it emits a DeprecationWarning and will be removed in 2.x, so switch to address: Address, or to address = Field[Address]() if you prefer an explicit field.

In your migration, store the value in a text or json column:

python
async def up(self):
    async with await self.schema.create("users") as table:
        table.id()
        table.string("name")
        table.text("address").nullable()
        table.timestamps()

Reading ​

python
user = await User.where("email", "admin@example.com").first()

isinstance(user.address, Address)   # True
user.address.city                   # "Sydney"
user.address.country                # "Australia"

Inserting ​

When creating a record, you can pass an Address, a plain dict, or nothing at all:

python
await User.create({
    "name": "Joe",
    "address": Address(street="123 Main St", city="Sydney", state="NSW", country="Australia"),
})

await User.create({
    "name": "Jane",
    "address": {"street": "456 Queen St", "city": "Melbourne", "state": "VIC", "country": "Australia"},
})

await User.create({"name": "Guest"})