Skip to content

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.

ts
// 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.
ts
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 values

Declaring Casts

Add a static casts object to your model. Keys are column names, values are cast type strings:

ts
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

CastD1 typeJS typeGet exampleSet example
'boolean'INTEGER (0/1)boolean1truetrue1
'integer'INTEGERnumber"42"423.73
'float'REALnumber"3.14"3.14passthrough
'string'TEXTstring123"123"passthrough
'datetime'TEXT (ISO 8601)Date"2026-01-15T..."DateDate → ISO string
'date'TEXT (YYYY-MM-DD)Date"2026-01-15"Date (midnight UTC)Date"2026-01-15"
'timestamp'INTEGER (unix)Date1700000000DateDate1700000000
'json'TEXTunknown'{"a":1}'{a: 1}{a: 1}'{"a":1}'
'array'TEXTunknown[]'["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.

ts
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 string

Custom Casts

For types not covered by the built-in casts, implement the AttributeCast interface:

ts
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.

ts
const model = new Post({ is_published: null })
model.get('is_published')  // null (not false)

toObject() vs toRaw()

MethodReturnsUse case
toObject()Cast (application) valuesAPI responses, UI rendering
toRaw()Dehydrated (DB-safe) valuesManual SQL, debugging, serialization
ts
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.

Released under the MIT License.