Casts & Serialization¶
A database column has a database type — TEXT, INTEGER, TIMESTAMP — but that's rarely the type
you want to work with in Python. A casts declaration bridges the two: it says "this column is stored
as JSON but I want a dict", or "this is stored as text but I want a Decimal", and the model does
the conversion for you on every read and write. Alongside casts, a model tracks what changed since it
was loaded, and knows how to serialize itself to a plain dict or JSON for an API response — the two
things you reach for constantly once a model leaves the database.
Casts & change tracking¶
post.was_changed("title") # since the last save
post.get_original("title")
fresh = await post.fresh() # re-fetch from the database
Casts include datetime, bool, int, json/array, collection, object, decimal:<scale>,
native enum, stringable, and (via the container) encrypted/encrypted:array/encrypted:json
and hashed.
The full cast set¶
from decimal import Decimal
class Order(Model):
__fields__ = {
"items": list, "meta": dict, "settings": dict,
"total": str, "notes": str, "reference": str,
}
__casts__ = {
"items": "array", # JSON column <-> list
"meta": "json", # JSON column <-> dict (alias of "array")
"settings": "collection", # JSON column <-> arvel.support.Collection
"total": "decimal:2", # <-> decimal.Decimal, quantized to 2 places on set
"notes": "encrypted:array", # ciphertext at rest, list/dict in Python
"reference": "stringable", # <-> arvel.support.Stringable
}
array/json— a JSON column stored asTEXT(the cast owns (de)serialization), read back as adict/list.arrayandjsonare the same cast under two names.collection— likearray, but reads back as anarvel.support.Collectioninstead of a plainlist; writing accepts aCollectionor any iterable.object— reads back as atypes.SimpleNamespace(dotted access:order.settings.theme); writing accepts aSimpleNamespaceor a plaindict(serialized viavars()).decimal:<scale>— a realdecimal.Decimal, quantized to<scale>places on write (and re-quantized on read). You get aDecimal, not a formatted string, so arithmetic on a money column doesn't round-trip through string parsing.encrypted:array/encrypted:json— likeencrypted, but serialize-aware: the plaintext is adict/list, not just a string. Ciphertext at rest, plaintext in Python — needsencrypterbound in the container (same asencrypted).stringable— reads back as anarvel.support.Stringable(a fluent string wrapper); writing accepts aStringableor a plainstr.- No separate
immutable_datetime. arvel'sdatetimecast already returns the immutable,whenever-basedDate, so there's no mutable-vs-immutable distinction to pick between — every datetime you read back is immutable by default.
Dates & timestamps¶
A datetime cast (and created_at/updated_at/deleted_at, plus any field declared with a
datetime type) is stored in the database as a real timezone-aware DateTime column and reads
back as an arvel Date — not an ISO string. This means a timestamped model persists
correctly on PostgreSQL timestamp with time zone columns, not just SQLite.
class Post(Model):
__fields__ = {"title": str, "published_at": datetime} # real DateTime column
__casts__ = {"published_at": "datetime"} # (optional — a datetime field auto-casts)
__timestamps__ = True
post = await Post.create(title="Hi", published_at=Date.now())
post.published_at # -> a Date (e.g. post.published_at.diff_for_humans())
post.created_at # -> a Date, not a string
You can set a datetime column from a Date, a stdlib datetime, or an ISO-8601 string — all are
normalized to a real datetime on write. In JSON responses a Date is serialized to an ISO-8601 string
automatically.
Serialize with to_dict() (honors __hidden__/__visible__/__appends__) or to_json()
for a JSON string of the same shape — Date, Decimal, and Enum values are encoded for you:
post.to_dict() # {"title": ..., "slug": ...} — hidden keys dropped
post.to_json() # '{"title": ..., "slug": ...}'
For richer, versioned API output — renamed fields, conditional inclusion, wrapping — reach for a
JsonResource instead of hand-shaping to_dict().
Common mistakes & gotchas¶
- Declaring a cast without a column to back it. A
json/arraycast needs a column that can hold serialized text; pair the cast with a matching__fields__entry so the migration creates the right column type. - Expecting a mutable datetime. Reads come back as an immutable
Date— apost.published_at.add(days=1)returns a newDate, it doesn't mutate in place. encryptedcasts without an encrypter.encrypted/encrypted:json/encrypted:arrayandhashedresolve their engine from the container — they raise if nothing is bound. See Encryption and Hashing.- Comparing a
decimalcast to afloat. The cast gives you aDecimal; compare and compute withDecimalvalues (or use the Money type) rather than mixing in floats.
See also¶
- Migrations & Schema — the column types a cast reads and writes.
- API Resources — structured serialization beyond
to_dict(). - Dates & Time — the
Datetype thedatetimecast returns. - Encryption · Hashing — the engines behind
encrypted/hashed.