-
Notifications
You must be signed in to change notification settings - Fork 4
Gradle Kotlinx Serialization Support
Kobby
supports Kotlinx Serialization
to provide an ability to generate Multiplatform GraphQL DSL Client since release 3.0.0
.
- Kobby at least version 3.0.0 is required.
- Gradle at least version 8.0 is required.
- Kotlin at least version 1.8.0 is required.
- Kotlinx Serialization at least 1.5.0 is required.
- Ktor at least version 2.0.0 is required for default adapters.
To enable Kotlinx Serialization support just add org.jetbrains.kotlinx:kotlinx-serialization-json
dependency
and configure serialization plugin:
Gradle
plugins {
kotlin("jvm") version "1.8.20"
kotlin("plugin.serialization") version "1.8.20"
id("io.github.ermadmi78.kobby") version "3.0.0"
}
dependencies {
// Add this dependency to enable Kotlinx Serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
}
You can explicitly enable (or disable) Kotlinx Serialization support in the generated code, but you still need to
add kotlinx-serialization-json
dependency and configure serialization plugin. In addition to the "implicit setup" you
can add:
Gradle
kobby {
kotlin {
dto {
serialization {
// Is Kotlinx Serialization enabled.
// By default, "true" if "org.jetbrains.kotlinx:kotlinx-serialization-json" artifact
// is in the project dependencies.
enabled = true
// Name of the class descriptor property for polymorphic serialization.
classDiscriminator = "__typename"
// Specifies whether encounters of unknown properties in the input JSON
// should be ignored instead of throwing SerializationException.
ignoreUnknownKeys = true
// Specifies whether default values of Kotlin properties should be encoded to JSON.
encodeDefaults = false
// Specifies whether resulting JSON should be pretty-printed.
prettyPrint = false
}
}
}
}
The Kotlinx Serialization entry point in the generated DSL is placed near
the DSL context entry point (root
file xxx.kt
, where xxx
is the name of the context). For example, for a context named cinema
it would look like
this:
cinema.kt
/**
* Default entry point to work with JSON serialization.
*/
public val cinemaJson: Json = Json {
classDiscriminator = "__typename"
ignoreUnknownKeys = true
encodeDefaults = false
prettyPrint = false
serializersModule = SerializersModule {
polymorphic(EntityDto::class) {
subclass(FilmDto::class)
subclass(ActorDto::class)
subclass(CountryDto::class)
}
polymorphic(TaggableDto::class) {
subclass(FilmDto::class)
subclass(ActorDto::class)
}
polymorphic(NativeDto::class) {
subclass(FilmDto::class)
subclass(ActorDto::class)
}
}
}
public fun cinemaContextOf(adapter: CinemaAdapter): CinemaContext = CinemaContextImpl(adapter)
The default adapters use cinemaJson
to provide Kotlinx Serialization.
Just pass cinemaJson
to the Ktor Client content negotiation plugin:
val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(cinemaJson)
}
}
val context = cinemaContextOf(
CinemaSimpleKtorAdapter(client, "http://localhost:8080/graphql")
)
val client = HttpClient(CIO) {
install(WebSockets)
}
val context = cinemaContextOf(
CinemaCompositeKtorAdapter(
client,
"http://localhost:8080/graphql",
"ws://localhost:8080/subscriptions"
)
)
You don't have to do anything because cinemaJson
is configured as the default value for the mapper
argument:
public open class CinemaCompositeKtorAdapter(
protected val client: HttpClient,
protected val httpUrl: String,
protected val webSocketUrl: String,
protected val mapper: Json = cinemaJson, // cinemaJson is configured by default!
// Skipped
) : CinemaAdapter {
// Skipped
}
You can configure
a custom serializer
to any type associated with a scalar. For example, let's define a Date
scalar in our
schema and associate it with a java.time.LocalDate
.
scalar Date
type Query {
extract: Date!
}
First, we must write custom serializer for java.time.LocalDate
:
object LocalDateSerializer : KSerializer<LocalDate> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: LocalDate) =
encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): LocalDate =
LocalDate.parse(decoder.decodeString())
}
Second, we must bind java.time.LocalDate
to Date
scalar and set up LocalDateSerializer
for it:
Gradle (see scalar mapping)
kobby {
kotlin {
scalars = mapOf(
"Date" to typeOf("java.time", "LocalDate")
.serializer(
"io.github.ermadmi78.kobby.cinema.api.kobby.kotlin.dto", // package name
"LocalDateSerializer" // class name
)
)
}
}
During the development process, it turned out that the Kotlinx Serialization engine does not like type Any
at all.
To get around this limitation, the plugin replaces type Any
in the generated code according to the following rules:
-
Map<String, Any?>
-> JsonObject -
List<Any?>
-> JsonArray -
Any?
-> JsonElement
For example, the GraphQL request DTO for Jackson serialization engine looks like this:
public data class CinemaRequest(
public val query: String,
@JsonInclude(value = JsonInclude.Include.NON_EMPTY)
public val variables: Map<String, Any?>? = null,
@JsonInclude(value = JsonInclude.Include.NON_ABSENT)
public val operationName: String? = null,
)
But the same DTO for Kotlinx Serialization engine looks like this:
@Serializable
public data class CinemaRequest(
public val query: String,
public val variables: JsonObject? = null,
public val operationName: String? = null,
)
Such a replacement leads to the fact that it is impossible to generate DTO classes that can be serialized using Jackson and Kotlinx Serialization at the same time. Therefore, you will need to choose one of the serialization engines and use only that.