Skip to content

Commit

Permalink
Create Json-specific kotlinx.serialization converter factory
Browse files Browse the repository at this point in the history
  • Loading branch information
0neel committed Jun 8, 2024
1 parent 7d01f21 commit 63a57d3
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 2 deletions.
38 changes: 38 additions & 0 deletions retrofit-converters/kotlinx-serialization-json/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# kotlinx.serialization Converter

A `Converter` which uses [kotlinx.serialization.json][1] for serialization.

Given a `Json`, call `asConverterFactory()` in order to
create a `Converter.Factory`.

```kotlin
val retrofit = Retrofit.Builder()
.baseUrl("https://example.com/")
.addConverterFactory(Json.asConverterFactory())
.build()
```


## Download

Download [the latest JAR][2] or grab via [Maven][3]:
```xml
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-kotlinx-serialization-json</artifactId>
<version>latest.version</version>
</dependency>
```
or [Gradle][3]:
```groovy
implementation 'com.squareup.retrofit2:converter-kotlinx-serialization-json:latest.version'
```

Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].



[1]: https://github.com/Kotlin/kotlinx.serialization
[2]: https://search.maven.org/remote_content?g=com.squareup.retrofit2&a=converter-kotlinx-serialization-json&v=LATEST
[3]: http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.squareup.retrofit2%22%20a%3A%22converter-kotlinx-serialization-json%22
[snap]: https://s01.oss.sonatype.org/content/repositories/snapshots/
16 changes: 16 additions & 0 deletions retrofit-converters/kotlinx-serialization-json/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apply plugin: 'org.jetbrains.kotlin.jvm'
apply plugin: 'org.jetbrains.kotlin.plugin.serialization'
apply plugin: 'com.vanniktech.maven.publish'
apply plugin: 'org.jetbrains.dokka'

dependencies {
implementation project(':retrofit-converters:kotlinx-serialization')

api projects.retrofit
api libs.kotlinx.serialization.json

testImplementation libs.junit
testImplementation libs.okhttp.mockwebserver
testImplementation libs.kotlinx.serialization.proto
testImplementation libs.kotlinx.serialization.json
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
POM_ARTIFACT_ID=converter-kotlinx-serialization-json
POM_NAME=Converter: kotlinx.serialization.json
POM_DESCRIPTION=A Retrofit Converter which uses kotlinx.serialization.json for serialization.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package retrofit2.converter.kotlinx.serialization.json

import kotlinx.serialization.json.Json
import okhttp3.MediaType
import retrofit2.Converter
import retrofit2.converter.kotlinx.serialization.Factory
import retrofit2.converter.kotlinx.serialization.Serializer

/**
* Return a [Converter.Factory] which uses Kotlin serialization for Json-based payloads.
*
* Because Kotlin serialization is so flexible in the types it supports, this converter assumes
* that it can handle all types. If you are mixing this with something else, you must add this
* instance last to allow the other converters a chance to see their types.
*/
@JvmName("create")
fun Json.asConverterFactory(): Converter.Factory {
val contentType = MediaType.get("application/json; charset=UTF-8")
return Factory(contentType, Serializer.FromString(this))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package retrofit2.converter.kotlinx.serialization.json

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST

class KotlinxJsonSerializationConverterFactoryContextualListTest {
@get:Rule
val server = MockWebServer()

private lateinit var service: Service

interface Service {
@GET("/")
fun deserialize(): Call<List<User>>

@POST("/")
fun serialize(@Body users: List<User>): Call<Void?>
}

data class User(val name: String)

object UserSerializer : KSerializer<User> {
override val descriptor = PrimitiveSerialDescriptor("User", PrimitiveKind.STRING)

override fun deserialize(decoder: Decoder): User =
decoder.decodeSerializableValue(UserResponse.serializer()).run {
User(name)
}

override fun serialize(encoder: Encoder, value: User): Unit =
encoder.encodeSerializableValue(UserResponse.serializer(), UserResponse(value.name))

@Serializable
private data class UserResponse(val name: String)
}

private val json = Json {
serializersModule = SerializersModule {
contextual(UserSerializer)
}
}

@Before
fun setUp() {
val retrofit = Retrofit.Builder()
.baseUrl(server.url("/"))
.addConverterFactory(json.asConverterFactory())
.build()
service = retrofit.create(Service::class.java)
}

@Test
fun deserialize() {
server.enqueue(MockResponse().setBody("""[{"name":"Bob"}]"""))
val user = service.deserialize().execute().body()!!
Assert.assertEquals(listOf(User("Bob")), user)
}

@Test
fun serialize() {
server.enqueue(MockResponse())
service.serialize(listOf(User("Bob"))).execute()
val request = server.takeRequest()
Assert.assertEquals("""[{"name":"Bob"}]""", request.body.readUtf8())
Assert.assertEquals("application/json; charset=UTF-8", request.headers["Content-Type"])
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package retrofit2.converter.kotlinx.serialization.json

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST

class KotlinxSerializationJsonConverterFactoryContextualTest {
@get:Rule
val server = MockWebServer()

private lateinit var service: Service

interface Service {
@GET("/")
fun deserialize(): Call<User>

@POST("/")
fun serialize(@Body user: User): Call<Void?>
}

data class User(val name: String)

object UserSerializer : KSerializer<User> {
override val descriptor = PrimitiveSerialDescriptor("User", PrimitiveKind.STRING)

override fun deserialize(decoder: Decoder): User =
decoder.decodeSerializableValue(UserResponse.serializer()).run {
User(name)
}

override fun serialize(encoder: Encoder, value: User): Unit =
encoder.encodeSerializableValue(UserResponse.serializer(), UserResponse(value.name))

@Serializable
private data class UserResponse(val name: String)
}

private val json = Json {
serializersModule = SerializersModule {
contextual(UserSerializer)
}
}

@Before
fun setUp() {
val retrofit = Retrofit.Builder()
.baseUrl(server.url("/"))
.addConverterFactory(json.asConverterFactory())
.build()
service = retrofit.create(Service::class.java)
}

@Test
fun deserialize() {
server.enqueue(MockResponse().setBody("""{"name":"Bob"}"""))
val user = service.deserialize().execute().body()!!
assertEquals(User("Bob"), user)
}

@Test
fun serialize() {
server.enqueue(MockResponse())
service.serialize(User("Bob")).execute()
val request = server.takeRequest()
assertEquals("""{"name":"Bob"}""", request.body.readUtf8())
assertEquals("application/json; charset=UTF-8", request.headers["Content-Type"])
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package retrofit2.converter.kotlinx.serialization.json

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST

class KotlinxSerializationJsonConverterFactoryTest {
@get:Rule val server = MockWebServer()

private lateinit var service: Service

interface Service {
@GET("/") fun deserialize(): Call<User>
@POST("/") fun serialize(@Body user: User): Call<Void?>
}

@Serializable
data class User(val name: String)

@Before fun setUp() {
val retrofit = Retrofit.Builder()
.baseUrl(server.url("/"))
.addConverterFactory(Json.asConverterFactory())
.build()
service = retrofit.create(Service::class.java)
}

@Test fun deserialize() {
server.enqueue(MockResponse().setBody("""{"name":"Bob"}"""))
val user = service.deserialize().execute().body()!!
assertEquals(User("Bob"), user)
}

@Test fun serialize() {
server.enqueue(MockResponse())
service.serialize(User("Bob")).execute()
val request = server.takeRequest()
assertEquals("""{"name":"Bob"}""", request.body.readUtf8())
assertEquals("application/json; charset=UTF-8", request.headers["Content-Type"])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import okhttp3.ResponseBody
import retrofit2.Converter
import retrofit2.Retrofit

internal class Factory(
class Factory(
private val contentType: MediaType,
private val serializer: Serializer
) : Converter.Factory() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import okhttp3.MediaType
import okhttp3.RequestBody
import okhttp3.ResponseBody

internal sealed class Serializer {
abstract class Serializer {
abstract fun <T> fromResponseBody(loader: DeserializationStrategy<T>, body: ResponseBody): T
abstract fun <T> toRequestBody(contentType: MediaType, saver: SerializationStrategy<T>, value: T): RequestBody

Expand Down
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ include ':retrofit-converters:java8'
include ':retrofit-converters:jaxb'
include ':retrofit-converters:jaxb3'
include ':retrofit-converters:kotlinx-serialization'
include ':retrofit-converters:kotlinx-serialization-json'
include ':retrofit-converters:moshi'
include ':retrofit-converters:protobuf'
include ':retrofit-converters:scalars'
Expand Down

0 comments on commit 63a57d3

Please sign in to comment.