0
# Column Type Adapters
1
2
SQLDelight's column adapter system provides bidirectional type conversion between Kotlin types and database column types. It enables custom serialization/deserialization and provides built-in adapters for common use cases like enums and complex types.
3
4
## Capabilities
5
6
### Column Adapter Interface
7
8
Core interface for bidirectional type conversion between Kotlin and database types.
9
10
```kotlin { .api }
11
/**
12
* Marshal and map the type T to and from a database type S which is one of
13
* Long, Double, String, ByteArray
14
* @param T The Kotlin type to convert to/from
15
* @param S The database type (Long, Double, String, or ByteArray)
16
*/
17
interface ColumnAdapter<T : Any, S> {
18
/**
19
* Convert database value to Kotlin type
20
* @param databaseValue The value from the database column
21
* @returns databaseValue decoded as type T
22
*/
23
fun decode(databaseValue: S): T
24
25
/**
26
* Convert Kotlin type to database value
27
* @param value The Kotlin value to store
28
* @returns value encoded as database type S
29
*/
30
fun encode(value: T): S
31
}
32
```
33
34
**Usage Examples:**
35
36
```kotlin
37
import app.cash.sqldelight.ColumnAdapter
38
39
// Custom date adapter using Long for timestamp storage
40
class DateAdapter : ColumnAdapter<Date, Long> {
41
override fun decode(databaseValue: Long): Date {
42
return Date(databaseValue)
43
}
44
45
override fun encode(value: Date): Long {
46
return value.time
47
}
48
}
49
50
// JSON adapter for complex objects using String storage
51
class JsonAdapter<T>(
52
private val serializer: KSerializer<T>
53
) : ColumnAdapter<T, String> {
54
override fun decode(databaseValue: String): T {
55
return Json.decodeFromString(serializer, databaseValue)
56
}
57
58
override fun encode(value: T): String {
59
return Json.encodeToString(serializer, value)
60
}
61
}
62
63
// UUID adapter using String storage
64
class UuidAdapter : ColumnAdapter<UUID, String> {
65
override fun decode(databaseValue: String): UUID {
66
return UUID.fromString(databaseValue)
67
}
68
69
override fun encode(value: UUID): String {
70
return value.toString()
71
}
72
}
73
74
// Usage in generated code context
75
val database = Database(
76
driver = driver,
77
userAdapter = User.Adapter(
78
id = UuidAdapter(),
79
createdAt = DateAdapter(),
80
preferences = JsonAdapter(UserPreferences.serializer())
81
)
82
)
83
```
84
85
### Built-in Enum Column Adapter
86
87
Pre-built adapter for mapping enum classes to database strings using enum names.
88
89
```kotlin { .api }
90
/**
91
* A ColumnAdapter which maps the enum class T to a string in the database
92
* @param enumValues Array of all enum values for the type T
93
*/
94
class EnumColumnAdapter<T : Enum<T>>(
95
private val enumValues: Array<out T>
96
) : ColumnAdapter<T, String> {
97
/**
98
* Convert database string to enum value by matching enum name
99
* @param databaseValue The enum name string from database
100
* @returns The enum value with matching name
101
* @throws NoSuchElementException if no enum value matches the database string
102
*/
103
override fun decode(databaseValue: String): T
104
105
/**
106
* Convert enum value to database string using enum name
107
* @param value The enum value to convert
108
* @returns The name property of the enum value
109
*/
110
override fun encode(value: T): String
111
}
112
113
/**
114
* Factory function to create EnumColumnAdapter for reified enum type
115
* A ColumnAdapter which maps the enum class T to a string in the database
116
* @returns EnumColumnAdapter instance for type T
117
*/
118
inline fun <reified T : Enum<T>> EnumColumnAdapter(): EnumColumnAdapter<T>
119
```
120
121
**Usage Examples:**
122
123
```kotlin
124
import app.cash.sqldelight.EnumColumnAdapter
125
126
// Define enum type
127
enum class UserStatus {
128
ACTIVE, INACTIVE, SUSPENDED, DELETED
129
}
130
131
enum class Priority {
132
LOW, MEDIUM, HIGH, CRITICAL
133
}
134
135
// Create adapters using factory function (preferred)
136
val statusAdapter = EnumColumnAdapter<UserStatus>()
137
val priorityAdapter = EnumColumnAdapter<Priority>()
138
139
// Create adapters manually
140
val manualStatusAdapter = EnumColumnAdapter(UserStatus.values())
141
142
// Usage in database schema
143
val database = Database(
144
driver = driver,
145
userAdapter = User.Adapter(
146
status = statusAdapter
147
),
148
taskAdapter = Task.Adapter(
149
priority = priorityAdapter
150
)
151
)
152
153
// The adapter handles conversion automatically
154
val activeUsers = userQueries.selectByStatus(UserStatus.ACTIVE).executeAsList()
155
// Database query: SELECT * FROM users WHERE status = 'ACTIVE'
156
157
userQueries.updateStatus(userId = 1, status = UserStatus.SUSPENDED)
158
// Database query: UPDATE users SET status = 'SUSPENDED' WHERE id = 1
159
160
// Error handling for invalid database values
161
try {
162
val invalidUser = userQueries.selectById(invalidId).executeAsOne()
163
} catch (e: NoSuchElementException) {
164
// Thrown if database contains enum name not in current enum definition
165
println("Invalid enum value in database: ${e.message}")
166
}
167
```
168
169
### Complex Type Adapters
170
171
Examples of adapters for handling complex data types and serialization scenarios.
172
173
**Usage Examples:**
174
175
```kotlin
176
import app.cash.sqldelight.ColumnAdapter
177
import kotlinx.serialization.*
178
import kotlinx.serialization.json.*
179
180
// List adapter using JSON serialization
181
class ListAdapter<T>(
182
private val elementSerializer: KSerializer<T>
183
) : ColumnAdapter<List<T>, String> {
184
override fun decode(databaseValue: String): List<T> {
185
return Json.decodeFromString(ListSerializer(elementSerializer), databaseValue)
186
}
187
188
override fun encode(value: List<T>): String {
189
return Json.encodeToString(ListSerializer(elementSerializer), value)
190
}
191
}
192
193
// Map adapter for key-value storage
194
class MapAdapter<K, V>(
195
private val keySerializer: KSerializer<K>,
196
private val valueSerializer: KSerializer<V>
197
) : ColumnAdapter<Map<K, V>, String> {
198
override fun decode(databaseValue: String): Map<K, V> {
199
return Json.decodeFromString(
200
MapSerializer(keySerializer, valueSerializer),
201
databaseValue
202
)
203
}
204
205
override fun encode(value: Map<K, V>): String {
206
return Json.encodeToString(
207
MapSerializer(keySerializer, valueSerializer),
208
value
209
)
210
}
211
}
212
213
// BigDecimal adapter using String for precision
214
class BigDecimalAdapter : ColumnAdapter<BigDecimal, String> {
215
override fun decode(databaseValue: String): BigDecimal {
216
return BigDecimal(databaseValue)
217
}
218
219
override fun encode(value: BigDecimal): String {
220
return value.toPlainString()
221
}
222
}
223
224
// Instant adapter using Long for epoch milliseconds
225
class InstantAdapter : ColumnAdapter<Instant, Long> {
226
override fun decode(databaseValue: Long): Instant {
227
return Instant.fromEpochMilliseconds(databaseValue)
228
}
229
230
override fun encode(value: Instant): Long {
231
return value.toEpochMilliseconds()
232
}
233
}
234
235
// Usage with complex types
236
@Serializable
237
data class UserPreferences(
238
val theme: String,
239
val notifications: Boolean,
240
val language: String
241
)
242
243
@Serializable
244
data class ContactInfo(
245
val email: String,
246
val phone: String?,
247
val socialLinks: Map<String, String>
248
)
249
250
val database = Database(
251
driver = driver,
252
userAdapter = User.Adapter(
253
tags = ListAdapter(String.serializer()),
254
preferences = JsonAdapter(UserPreferences.serializer()),
255
contactInfo = JsonAdapter(ContactInfo.serializer()),
256
metadata = MapAdapter(String.serializer(), String.serializer()),
257
balance = BigDecimalAdapter(),
258
lastSeen = InstantAdapter()
259
)
260
)
261
```
262
263
### Error Handling in Adapters
264
265
Implement robust error handling for data conversion failures.
266
267
**Usage Examples:**
268
269
```kotlin
270
import app.cash.sqldelight.ColumnAdapter
271
272
// Adapter with comprehensive error handling
273
class SafeJsonAdapter<T>(
274
private val serializer: KSerializer<T>,
275
private val defaultValue: T
276
) : ColumnAdapter<T, String> {
277
override fun decode(databaseValue: String): T {
278
return try {
279
Json.decodeFromString(serializer, databaseValue)
280
} catch (e: SerializationException) {
281
println("Failed to decode JSON: $databaseValue, using default: $defaultValue")
282
defaultValue
283
} catch (e: IllegalArgumentException) {
284
println("Invalid JSON format: $databaseValue, using default: $defaultValue")
285
defaultValue
286
}
287
}
288
289
override fun encode(value: T): String {
290
return try {
291
Json.encodeToString(serializer, value)
292
} catch (e: SerializationException) {
293
println("Failed to encode value: $value, using default JSON")
294
Json.encodeToString(serializer, defaultValue)
295
}
296
}
297
}
298
299
// Enum adapter with fallback for unknown values
300
class SafeEnumAdapter<T : Enum<T>>(
301
private val enumValues: Array<out T>,
302
private val defaultValue: T
303
) : ColumnAdapter<T, String> {
304
override fun decode(databaseValue: String): T {
305
return enumValues.firstOrNull { it.name == databaseValue }
306
?: run {
307
println("Unknown enum value: $databaseValue, using default: $defaultValue")
308
defaultValue
309
}
310
}
311
312
override fun encode(value: T): String {
313
return value.name
314
}
315
}
316
317
// Numeric adapter with validation
318
class ValidatedIntAdapter(
319
private val min: Int = Int.MIN_VALUE,
320
private val max: Int = Int.MAX_VALUE
321
) : ColumnAdapter<Int, Long> {
322
override fun decode(databaseValue: Long): Int {
323
val intValue = databaseValue.toInt()
324
return when {
325
intValue < min -> {
326
println("Value $intValue below minimum $min, clamping")
327
min
328
}
329
intValue > max -> {
330
println("Value $intValue above maximum $max, clamping")
331
max
332
}
333
else -> intValue
334
}
335
}
336
337
override fun encode(value: Int): Long {
338
val clampedValue = value.coerceIn(min, max)
339
if (clampedValue != value) {
340
println("Value $value outside range [$min, $max], clamped to $clampedValue")
341
}
342
return clampedValue.toLong()
343
}
344
}
345
346
// Usage with error handling
347
val database = Database(
348
driver = driver,
349
userAdapter = User.Adapter(
350
status = SafeEnumAdapter(UserStatus.values(), UserStatus.ACTIVE),
351
preferences = SafeJsonAdapter(
352
UserPreferences.serializer(),
353
UserPreferences(theme = "default", notifications = true, language = "en")
354
),
355
score = ValidatedIntAdapter(min = 0, max = 100)
356
)
357
)
358
```
359
360
### Migration and Schema Evolution
361
362
Handle schema changes and data migration with column adapters.
363
364
**Usage Examples:**
365
366
```kotlin
367
import app.cash.sqldelight.ColumnAdapter
368
369
// Versioned adapter for handling schema evolution
370
class VersionedUserPreferencesAdapter : ColumnAdapter<UserPreferences, String> {
371
override fun decode(databaseValue: String): UserPreferences {
372
return try {
373
// Try current version first
374
Json.decodeFromString<UserPreferences>(databaseValue)
375
} catch (e: SerializationException) {
376
try {
377
// Fallback to previous version
378
val oldPrefs = Json.decodeFromString<OldUserPreferences>(databaseValue)
379
migrateFromOldVersion(oldPrefs)
380
} catch (e: SerializationException) {
381
// Final fallback to defaults
382
UserPreferences.default()
383
}
384
}
385
}
386
387
override fun encode(value: UserPreferences): String {
388
return Json.encodeToString(value)
389
}
390
391
private fun migrateFromOldVersion(old: OldUserPreferences): UserPreferences {
392
return UserPreferences(
393
theme = old.theme,
394
notifications = old.notifications,
395
language = old.language ?: "en", // New field with default
396
// New fields get default values
397
darkMode = false,
398
fontSize = FontSize.MEDIUM
399
)
400
}
401
}
402
403
// Backward-compatible enum adapter
404
class BackwardCompatibleStatusAdapter : ColumnAdapter<UserStatus, String> {
405
override fun decode(databaseValue: String): UserStatus {
406
return when (databaseValue) {
407
"ACTIVE" -> UserStatus.ACTIVE
408
"INACTIVE" -> UserStatus.INACTIVE
409
"SUSPENDED" -> UserStatus.SUSPENDED
410
"DELETED" -> UserStatus.DELETED
411
// Handle old enum values that no longer exist
412
"BANNED" -> UserStatus.SUSPENDED // Map old value to new equivalent
413
"PENDING" -> UserStatus.INACTIVE // Map old value to new equivalent
414
else -> {
415
println("Unknown status: $databaseValue, defaulting to INACTIVE")
416
UserStatus.INACTIVE
417
}
418
}
419
}
420
421
override fun encode(value: UserStatus): String {
422
return value.name
423
}
424
}
425
426
// Usage during database migration
427
val legacyDatabase = Database(
428
driver = driver,
429
userAdapter = User.Adapter(
430
status = BackwardCompatibleStatusAdapter(),
431
preferences = VersionedUserPreferencesAdapter()
432
)
433
)
434
435
// Migration script example
436
fun migrateUserData() {
437
val users = legacyDatabase.userQueries.selectAll().executeAsList()
438
users.forEach { user ->
439
// Adapters handle the conversion automatically
440
val updatedUser = user.copy(
441
// Any necessary transformations
442
)
443
legacyDatabase.userQueries.update(updatedUser)
444
}
445
}
446
```