Skip to content

Commit 0e4f358

Browse files
committed
Add a note about leaf-level contextuals and an example
1 parent bd3c42b commit 0e4f358

2 files changed

Lines changed: 55 additions & 0 deletions

File tree

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,48 @@ val customers: List<Customer> = Sql("SELECT * FROM customers").queryOf<Customer>
575575

576576
There are several other ways to do this, have a look at the [Custom Serializers](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#custom-serializers) section kotlinx-serialization documentation for more information.
577577

578+
### Contextual Serialization for BigDecimal and UUID
579+
580+
When querying for types like `BigDecimal` or `UUID` directly (without wrapping them in a data class), you cannot use them as the direct result type in `queryOf<T>()`. This is because kotlinx-serialization requires all types to either be annotated with `@Serializable` or be marked as `@Contextual`, and these Java types cannot be annotated.
581+
582+
For example, this will **fail**:
583+
```kotlin
584+
// This will throw: Serializer for class 'java.math.BigDecimal' is not found
585+
val total: BigDecimal = Sql("SELECT total FROM orders WHERE id = $id").queryOf<BigDecimal>().runOn(ctx)
586+
```
587+
588+
The error you'll see:
589+
```
590+
kotlinx.serialization.SerializationException: Serializer for class 'java.math.BigDecimal' is not found.
591+
Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied.
592+
```
593+
594+
**Why this happens:** Kotlin's serialization system uses compile-time code generation to create serializers for types marked `@Serializable`. For types that can't be annotated (like Java standard library classes), kotlinx-serialization provides a contextual serialization mechanism. However, this mechanism requires the type to be wrapped in a context that declares it as `@Contextual`.
595+
596+
**Solution:** Wrap the return type in a data class and use a `@Contextual` annotation:
597+
598+
```kotlin
599+
@Serializable
600+
data class BigDecimalWrapper(@Contextual val value: BigDecimal)
601+
602+
// Now this works:
603+
val result: List<BigDecimalWrapper> =
604+
Sql("SELECT total FROM orders WHERE id = $id").queryOf<BigDecimalWrapper>().runOn(ctx)
605+
val total: BigDecimal = result.first().value
606+
```
607+
608+
The same applies for `UUID`:
609+
```kotlin
610+
@Serializable
611+
data class UuidWrapper(@Contextual val value: UUID)
612+
613+
val result: List<UuidWrapper> =
614+
Sql("SELECT user_id FROM sessions WHERE token = $token").queryOf<UuidWrapper>().runOn(ctx)
615+
val userId: UUID = result.first().value
616+
```
617+
618+
Note that you can use `BigDecimal` and `UUID` directly as parameters in SQL queries without any wrapper since Terpal automatically handles encoding them (they're in the automatically-wrapped types list). The `@Contextual` wrapper is only needed when they appear as decoded result types.
619+
578620
### Custom Primitives
579621

580622
In some situations, with a custom datatype you may need to control how it is encoded in the database driver.

terpal-sql-jdbc/src/test/kotlin/io/exoquery/sql/postgres/EncodingSpec.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import io.exoquery.sql.Sql
66
import io.exoquery.controller.jdbc.JdbcControllers
77
import io.exoquery.controller.runOn
88
import io.kotest.core.spec.style.FreeSpec
9+
import io.kotest.matchers.bigdecimal.shouldBeEqualIgnoringScale
10+
import kotlinx.serialization.Contextual
11+
import kotlinx.serialization.Serializable
12+
import java.math.BigDecimal
913
import java.time.ZoneId
1014

1115
class EncodingSpec: FreeSpec({
@@ -66,6 +70,15 @@ class EncodingSpec: FreeSpec({
6670
verify(actual, JavaTestEntity.regular)
6771
}
6872

73+
"Single-Value Contextual Type" {
74+
@Serializable
75+
data class Wrapper(@Contextual val value: BigDecimal)
76+
Sql("DELETE FROM JavaTestEntity").action().runOn(ctx)
77+
insert(JavaTestEntity.regular).runOn(ctx)
78+
val actual = Sql("SELECT bigDecimalMan FROM JavaTestEntity").queryOf<Wrapper>().runOn(ctx).first()
79+
actual.value shouldBeEqualIgnoringScale JavaTestEntity.regular.bigDecimalMan
80+
}
81+
6982
"Encode/Decode Additional Java Types - empty" {
7083
Sql("DELETE FROM JavaTestEntity").action().runOn(ctx)
7184
insert(JavaTestEntity.empty).runOn(ctx)

0 commit comments

Comments
 (0)