Attribute Casting
D1 stores only four primitive types: TEXT, INTEGER, REAL, and BLOB. Attribute casting lets you declare how columns map to JavaScript types — d1-eloquent handles the conversion automatically in both directions.
// Reading — manual conversion scattered across your codebase
const row = results[0]
const isPublished = row.is_published === 1 // remember: D1 returns 0/1
const metadata = JSON.parse(row.metadata as string) // hope it's valid JSON
const createdAt = new Date(row.created_at as string) // don't forget this one
// Writing — mirror every conversion in reverse
await env.DB.prepare(
'INSERT INTO posts (id, is_published, metadata, created_at) VALUES (?, ?, ?, ?)'
).bind(
id,
post.isPublished ? 1 : 0, // boolean → integer
JSON.stringify(post.metadata), // object → JSON string
post.createdAt.toISOString() // Date → ISO string
).run()
// Every read site and every write site must agree on the conversion.
// Forget one? Silent data corruption.class Post extends BaseModel<PostAttrs> {
static table = 'posts'
static casts = {
is_published: 'boolean', // D1 INTEGER 0/1 ↔ JS boolean
metadata: 'json', // D1 TEXT ↔ JS object
}
}
// Reading — automatic, everywhere
post.get('is_published') // true (boolean, not 1)
post.get('metadata') // { key: "val" } (object, not string)
post.get('created_at') // Date instance (auto-cast for timestamps)
// Writing — automatic, everywhere
post.set({ is_published: false, metadata: { key: 'new' } })
await post.save() // d1-eloquent dehydrates to DB-safe valuesDeclaring Casts
Add a static casts object to your model. Keys are column names, values are cast type strings:
import { BaseModel } from '@orphnet/d1-eloquent'
interface PostAttrs {
id: string
title: string
is_published: boolean
metadata: Record<string, unknown>
view_count: number
created_at: Date
updated_at: Date
}
class Post extends BaseModel<PostAttrs> {
static table = 'posts'
static casts = {
is_published: 'boolean',
metadata: 'json',
view_count: 'integer',
}
}After casting, post.get('is_published') returns a boolean (not 0/1), and post.get('metadata') returns an object (not a JSON string).
Built-in Cast Types
| Cast | D1 type | JS type | Get example | Set example |
|---|---|---|---|---|
'boolean' | INTEGER (0/1) | boolean | 1 → true | true → 1 |
'integer' | INTEGER | number | "42" → 42 | 3.7 → 3 |
'float' | REAL | number | "3.14" → 3.14 | passthrough |
'string' | TEXT | string | 123 → "123" | passthrough |
'datetime' | TEXT (ISO 8601) | Date | "2026-01-15T..." → Date | Date → ISO string |
'date' | TEXT (YYYY-MM-DD) | Date | "2026-01-15" → Date (midnight UTC) | Date → "2026-01-15" |
'timestamp' | INTEGER (unix) | Date | 1700000000 → Date | Date → 1700000000 |
'json' | TEXT | unknown | '{"a":1}' → {a: 1} | {a: 1} → '{"a":1}' |
'array' | TEXT | unknown[] | '["a","b"]' → ["a","b"] | ["a","b"] → '["a","b"]' |
Automatic Timestamp Casting
When timestamps = true (the default), created_at and updated_at are automatically cast to datetime. When softDeletes = true, deleted_at is also auto-cast.
You do not need to declare these in static casts — they are registered automatically. If you do declare them, your cast takes priority.
class User extends BaseModel<UserAttrs> {
static table = 'users'
static timestamps = true // created_at/updated_at auto-cast to Date
static softDeletes = true // deleted_at auto-cast to Date
}
const user = await User.find(userId)
user.get('created_at') // Date instance, not a stringCustom Casts
For types not covered by the built-in casts, implement the AttributeCast interface:
import { BaseModel } from '@orphnet/d1-eloquent'
import type { AttributeCast } from '@orphnet/d1-eloquent'
const pointCast: AttributeCast<{ x: number; y: number }, { x: number; y: number }> = {
get: (value) => JSON.parse(value as string),
set: (value) => JSON.stringify(value),
}
class Marker extends BaseModel<MarkerAttrs> {
static table = 'markers'
static casts = {
position: pointCast,
}
}The get method transforms DB values → application values. The set method transforms application values → DB values.
Null Handling
null and undefined values pass through casts unchanged — no cast function is called. This matches SQL NULL semantics.
const model = new Post({ is_published: null })
model.get('is_published') // null (not false)toObject() vs toRaw()
| Method | Returns | Use case |
|---|---|---|
toObject() | Cast (application) values | API responses, UI rendering |
toRaw() | Dehydrated (DB-safe) values | Manual SQL, debugging, serialization |
const post = await Post.find(postId)
post.toObject()
// { id: "...", is_published: true, metadata: { key: "val" }, created_at: Date, ... }
post.toRaw()
// { id: "...", is_published: 1, metadata: '{"key":"val"}', created_at: "2026-01-15T...", ... }Revisions
Revision snapshots (before, after, diff) store raw (dehydrated) values. This ensures revisions contain DB-safe primitives that can be replayed reliably.