Custom Type Encoding
Using custom encoders and decoders for domain-specific types
When using param(...) to insert values into queries, ExoQuery relies on kotlinx.serialization to encode your data. This works well for standard types and @Serializable classes, but what if you have a custom domain type like PersonId(val value: Int) that you want to use directly in queries?
This is where custom encodings come in. Custom encodings allow you to bypass the kotlinx.serialization stack entirely and directly program how your custom types are written to a PreparedStatement (for encoding) or read from a ResultSet (for decoding). Once you’ve registered a custom encoder/decoder with your database controller, you can use paramCtx(...) to insert your custom type into queries—the controller will use your custom encoder to handle the value directly.
In short:
param(...)→ uses kotlinx.serialization (standard types,@Serializableclasses)paramCtx(...)→ uses your custom encoders registered on the controller (domain-specific types)
JDBC Custom Encoding
For JDBC, you can add custom encoders and decoders to your database controller:
import io.exoquery.controller.JdbcControllers
import io.exoquery.controller.jdbc.JdbcEncoderAny
import io.exoquery.controller.jdbc.JdbcDecoderAny
import java.sql.Types
// Define a custom type
data class PersonId(val value: Int)
// Create a controller with custom encoding
val myDatabase = object : JdbcControllers.Postgres(dataSource) {
override val additionalDecoders =
super.additionalDecoders + JdbcDecoderAny { ctx, i -> PersonId(ctx.row.getInt(i)) }
override val additionalEncoders =
super.additionalEncoders + JdbcEncoderAny(Types.INTEGER) { ctx, v: PersonId, i ->
ctx.stmt.setInt(i, v.value)
}
}
R2DBC Custom Encoding
For R2DBC, you can configure custom encoders and decoders through the encoding config:
import io.exoquery.controller.r2dbc.R2dbcControllers
import io.exoquery.controller.r2dbc.R2dbcEncodingConfig
import io.exoquery.controller.r2dbc.R2dbcBasicEncoding
// Define a custom type
data class PersonId(val value: Int)
// Create a controller with custom encoding
val controller = R2dbcControllers.Postgres(
encodingConfig = R2dbcEncodingConfig.Default(
encoders = setOf(
R2dbcBasicEncoding.IntEncoder.contramap { id: PersonId -> id.value }
),
decoders = setOf(
R2dbcBasicEncoding.IntDecoder.map { PersonId(it) }
)
),
connectionFactory = connectionFactory
)
Using Custom Types in Queries
Once you’ve configured custom encoding, you can use your custom types in queries with paramCtx:
val personId = PersonId(123)
val q = sql {
Table<Person>().filter { p -> p.id == paramCtx(personId) }
}
q.buildFor.Postgres().runOn(myDatabase)
//> SELECT p.id, p.name, p.age FROM Person p WHERE p.id = ?
Remember that for custom types used in entity classes, you’ll need to mark the field with @Contextual:
@Serializable
data class Person(
@Contextual val id: PersonId,
val name: String,
val age: Int
)