Skip to main content
GRDB provides automatic conformance to FetchableRecord and PersistableRecord for types that adopt Swift’s Codable protocol. This eliminates boilerplate code and makes working with database records seamless.

Basic Usage

Define a Codable type and add the record protocols:
struct Player: Codable {
    var id: Int64?
    var name: String
    var score: Int
}

// Add record protocol conformance
extension Player: FetchableRecord, MutablePersistableRecord {
    static let databaseTableName = "player"
    
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}

// That's it! No need to implement init(row:) or encode(to:)
try dbQueue.write { db in
    var player = Player(id: nil, name: "Arthur", score: 100)
    try player.insert(db)
    
    let players = try Player.fetchAll(db)
}
When your type conforms to Codable, GRDB automatically implements init(row:) and encode(to:) for you. Property names map directly to column names.

How It Works

When you conform to both Codable and FetchableRecord:
  • GRDB uses the Decodable conformance to implement init(row:)
  • Your CodingKeys map to database column names
When you conform to both Codable and PersistableRecord:
  • GRDB uses the Encodable conformance to implement encode(to:)
  • Your CodingKeys map to database column names
struct Player: Codable {
    var id: Int64?
    var name: String
    var score: Int
    
    // CodingKeys define the mapping
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case score
    }
}

Custom Column Mapping

Use custom CodingKeys to map properties to different column names:
struct Player: Codable {
    var id: Int64?
    var name: String
    var score: Int
    var teamId: Int64
    
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case score
        case teamId = "team_id"  // Maps to team_id column
    }
}

extension Player: FetchableRecord, MutablePersistableRecord {
    static let databaseTableName = "player"
    
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}

Snake Case Conversion

Automatically convert between snake_case columns and camelCase properties:
struct Player: Codable {
    var playerId: Int64?     // Maps to player_id
    var firstName: String    // Maps to first_name
    var lastName: String     // Maps to last_name
    var teamId: Int64        // Maps to team_id
}

extension Player: FetchableRecord, MutablePersistableRecord {
    static let databaseTableName = "player"
    
    // Enable snake_case conversion
    static let databaseColumnDecodingStrategy = DatabaseColumnDecodingStrategy.convertFromSnakeCase
    static let databaseColumnEncodingStrategy = DatabaseColumnEncodingStrategy.convertToSnakeCase
    
    mutating func didInsert(_ inserted: InsertionSuccess) {
        playerId = inserted.rowID
    }
}

Date Handling

Customize how dates are encoded and decoded:
struct Event: Codable {
    var id: Int64?
    var name: String
    var timestamp: Date
    var createdAt: Date
}

extension Event: FetchableRecord, MutablePersistableRecord {
    static let databaseTableName = "event"
    
    // Decode dates as Unix timestamps
    static func databaseDateDecodingStrategy(for column: String) -> DatabaseDateDecodingStrategy {
        switch column {
        case "timestamp":
            return .timeIntervalSince1970
        case "createdAt":
            return .deferredToDate  // Default format
        default:
            return .deferredToDate
        }
    }
    
    // Encode dates as Unix timestamps
    static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy {
        switch column {
        case "timestamp":
            return .timeIntervalSince1970
        default:
            return .deferredToDate
        }
    }
    
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}
DatabaseDateDecodingStrategy
enum
Available date decoding strategies:
  • .deferredToDate - Standard Date decoding (default)
  • .timeIntervalSince1970 - Unix timestamp in seconds
  • .timeIntervalSinceReferenceDate - Seconds since 2001-01-01
  • .millisecondsSince1970 - Unix timestamp in milliseconds
  • .iso8601 - ISO 8601 formatted string
  • .formatted(DateFormatter) - Custom date formatter
  • .custom((DatabaseValue) -> Date?) - Custom decoding closure
DatabaseDateEncodingStrategy
enum
Available date encoding strategies:
  • .deferredToDate - Standard Date encoding (default)
  • .timeIntervalSince1970 - Unix timestamp in seconds
  • .timeIntervalSinceReferenceDate - Seconds since 2001-01-01
  • .millisecondsSince1970 - Unix timestamp in milliseconds
  • .iso8601 - ISO 8601 formatted string
  • .formatted(DateFormatter) - Custom date formatter
  • .custom((Date, inout PersistenceContainer) throws -> Void) - Custom encoding closure

Data and Binary Handling

Customize how Data values are encoded and decoded:
struct Document: Codable {
    var id: Int64?
    var title: String
    var content: Data
}

extension Document: FetchableRecord, MutablePersistableRecord {
    static let databaseTableName = "document"
    
    // Decode Data from base64
    static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy {
        .custom { dbValue in
            guard let base64String = String.fromDatabaseValue(dbValue) else {
                return nil
            }
            return Data(base64Encoded: base64String)
        }
    }
    
    // Encode Data as base64
    static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy {
        .custom { data, container in
            container[column] = data.base64EncodedString()
        }
    }
    
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}

JSON Columns

Store complex types as JSON:
struct Settings: Codable {
    var theme: String
    var language: String
    var notifications: Bool
}

struct User: Codable {
    var id: Int64?
    var username: String
    var settings: Settings  // Stored as JSON
}

extension User: FetchableRecord, MutablePersistableRecord {
    static let databaseTableName = "user"
    
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}

// Create table with JSON column
try db.execute(sql: """
    CREATE TABLE user (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        username TEXT NOT NULL,
        settings TEXT NOT NULL  -- JSON stored as TEXT
    )
    """)

var user = User(
    id: nil,
    username: "arthur",
    settings: Settings(theme: "dark", language: "en", notifications: true)
)
try user.insert(db)

let fetched = try User.fetchOne(db, id: user.id)
print(fetched?.settings.theme)  // "dark"
GRDB automatically encodes and decodes nested Codable types as JSON. Just make sure your database column is of type TEXT.

Optional Properties

Optional properties work seamlessly:
struct Player: Codable {
    var id: Int64?
    var name: String
    var nickname: String?      // Optional column
    var score: Int
    var metadata: [String: String]?  // Optional JSON
}

extension Player: FetchableRecord, MutablePersistableRecord {
    static let databaseTableName = "player"
    
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}

var player = Player(
    id: nil,
    name: "Arthur",
    nickname: nil,  // NULL in database
    score: 100,
    metadata: nil
)
try player.insert(db)

Enums with Codable

Use enums for type-safe column values:
enum PlayerStatus: String, Codable {
    case active
    case inactive
    case suspended
}

struct Player: Codable {
    var id: Int64?
    var name: String
    var status: PlayerStatus
}

extension Player: FetchableRecord, MutablePersistableRecord {
    static let databaseTableName = "player"
    
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}

var player = Player(id: nil, name: "Arthur", status: .active)
try player.insert(db)

let activePlayers = try Player
    .filter(Column("status") == PlayerStatus.active.rawValue)
    .fetchAll(db)

Arrays and Collections

Store arrays as JSON:
struct Team: Codable {
    var id: Int64?
    var name: String
    var tags: [String]         // Stored as JSON array
    var scores: [Int]          // Stored as JSON array
}

extension Team: FetchableRecord, MutablePersistableRecord {
    static let databaseTableName = "team"
    
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}

var team = Team(
    id: nil,
    name: "Warriors",
    tags: ["sports", "basketball", "professional"],
    scores: [95, 102, 88, 110]
)
try team.insert(db)

let fetched = try Team.fetchOne(db, id: team.id)
print(fetched?.tags)  // ["sports", "basketball", "professional"]

Custom Encoding/Decoding Logic

You can still provide custom Codable implementations:
struct Player: Codable {
    var id: Int64?
    var name: String
    var score: Int
    
    // Custom initializer with validation
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decodeIfPresent(Int64.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
        
        let scoreValue = try container.decode(Int.self, forKey: .score)
        guard scoreValue >= 0 else {
            throw DecodingError.dataCorrupted(
                DecodingError.Context(
                    codingPath: [CodingKeys.score],
                    debugDescription: "Score must be non-negative"
                )
            )
        }
        score = scoreValue
    }
    
    // Custom encoder
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encodeIfPresent(id, forKey: .id)
        try container.encode(name.trimmingCharacters(in: .whitespaces), forKey: .name)
        try container.encode(score, forKey: .score)
    }
    
    enum CodingKeys: String, CodingKey {
        case id, name, score
    }
}

extension Player: FetchableRecord, MutablePersistableRecord {
    static let databaseTableName = "player"
    
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}

Partial Records

Use different types for insertion and fetching:
// Minimal type for insertion
struct NewPlayer: Encodable {
    var name: String
    var email: String
}

extension NewPlayer: MutablePersistableRecord {
    static let databaseTableName = "player"
}

// Complete type for fetching
struct Player: Decodable {
    var id: Int64
    var name: String
    var email: String
    var createdAt: Date      // Generated by database
    var score: Int           // Has default value
}

extension Player: FetchableRecord {
    static let databaseTableName = "player"
}

// Insert minimal data
var newPlayer = NewPlayer(name: "Arthur", email: "[email protected]")
try newPlayer.insert(db)

// Fetch complete data
let player = try Player.fetchAll(db)

UserInfo for Custom Context

Pass custom context to your Codable implementation:
let myContextKey = CodingUserInfoKey(rawValue: "myContext")!

struct Player: Codable {
    var id: Int64?
    var name: String
    
    init(from decoder: Decoder) throws {
        // Access custom context
        if let context = decoder.userInfo[myContextKey] as? String {
            print("Decoding context: \(context)")
        }
        
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decodeIfPresent(Int64.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
    }
    
    enum CodingKeys: CodingKey {
        case id, name
    }
}

extension Player: FetchableRecord, MutablePersistableRecord {
    static let databaseTableName = "player"
    
    // Provide user info for decoding
    static var databaseDecodingUserInfo: [CodingUserInfoKey: Any] {
        [myContextKey: "database"]
    }
    
    // Provide user info for encoding
    static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] {
        [myContextKey: "database"]
    }
    
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}

Full Example

Here’s a complete example with all the features:
import GRDB
import Foundation

// Define a Codable record type
struct Player: Codable {
    var id: Int64?
    var name: String
    var email: String
    var score: Int
    var metadata: PlayerMetadata?
    var createdAt: Date?
    
    struct PlayerMetadata: Codable {
        var level: Int
        var achievements: [String]
    }
    
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case email
        case score
        case metadata
        case createdAt = "created_at"
    }
}

// Add record protocol conformance
extension Player: FetchableRecord, MutablePersistableRecord {
    static let databaseTableName = "player"
    
    mutating func willInsert(_ db: Database) throws {
        createdAt = Date()
    }
    
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}

// Usage
let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")

try dbQueue.write { db in
    // Create table
    try db.execute(sql: """
        CREATE TABLE player (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            email TEXT NOT NULL UNIQUE,
            score INTEGER NOT NULL DEFAULT 0,
            metadata TEXT,
            created_at DATETIME
        )
        """)
    
    // Insert a player
    var player = Player(
        id: nil,
        name: "Arthur",
        email: "[email protected]",
        score: 100,
        metadata: Player.PlayerMetadata(
            level: 5,
            achievements: ["first_win", "high_score"]
        ),
        createdAt: nil
    )
    try player.insert(db)
    print("Inserted player with id: \(player.id!)")
    
    // Update
    player.score = 150
    try player.update(db)
    
    // Fetch
    let players = try Player.fetchAll(db)
    for p in players {
        print("\(p.name): \(p.score)")
        if let metadata = p.metadata {
            print("  Level \(metadata.level)")
            print("  Achievements: \(metadata.achievements.joined(separator: ", "))")
        }
    }
}

Best Practices

Always use Codable when possible: If your properties map naturally to database columns, use Codable to eliminate boilerplate. GRDB’s automatic implementations handle the common cases perfectly.
Use CodingKeys for column mapping: When column names don’t match property names, use custom CodingKeys rather than implementing init(row:) and encode(to:) manually.
Leverage snake_case conversion: If your database uses snake_case columns but your Swift code uses camelCase, enable automatic conversion with databaseColumnDecodingStrategy and databaseColumnEncodingStrategy.
JSON columns must be TEXT: When using nested Codable types, make sure the database column is defined as TEXT. GRDB stores JSON as text by default.
Codable works with both structs and classes: While structs are recommended for value semantics, you can use Codable with classes too. Just use PersistableRecord instead of MutablePersistableRecord.

Build docs developers (and LLMs) love