From bfc559194e8dcdc9ada37c5be6c5317b21bb0e94 Mon Sep 17 00:00:00 2001 From: Maksim Kovalev Date: Sat, 8 Jun 2024 18:53:51 +0200 Subject: [PATCH 1/7] Create Json-specific kotlinx.serialization converter factory --- .../kotlinx-serialization-json/README.md | 38 +++++++++ .../kotlinx-serialization-json/build.gradle | 16 ++++ .../gradle.properties | 3 + .../json/JsonConverterFactory.kt | 20 +++++ ...ationConverterFactoryContextualListTest.kt | 85 +++++++++++++++++++ ...ationJsonConverterFactoryContextualTest.kt | 85 +++++++++++++++++++ ...nxSerializationJsonConverterFactoryTest.kt | 51 +++++++++++ .../kotlinx/serialization/Factory.kt | 2 +- .../kotlinx/serialization/Serializer.kt | 2 +- settings.gradle | 1 + 10 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 retrofit-converters/kotlinx-serialization-json/README.md create mode 100644 retrofit-converters/kotlinx-serialization-json/build.gradle create mode 100644 retrofit-converters/kotlinx-serialization-json/gradle.properties create mode 100644 retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/JsonConverterFactory.kt create mode 100644 retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxJsonSerializationConverterFactoryContextualListTest.kt create mode 100644 retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxSerializationJsonConverterFactoryContextualTest.kt create mode 100644 retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxSerializationJsonConverterFactoryTest.kt diff --git a/retrofit-converters/kotlinx-serialization-json/README.md b/retrofit-converters/kotlinx-serialization-json/README.md new file mode 100644 index 0000000000..cabd31de76 --- /dev/null +++ b/retrofit-converters/kotlinx-serialization-json/README.md @@ -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 + + com.squareup.retrofit2 + converter-kotlinx-serialization-json + latest.version + +``` +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/ diff --git a/retrofit-converters/kotlinx-serialization-json/build.gradle b/retrofit-converters/kotlinx-serialization-json/build.gradle new file mode 100644 index 0000000000..c9f60fdd81 --- /dev/null +++ b/retrofit-converters/kotlinx-serialization-json/build.gradle @@ -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 +} diff --git a/retrofit-converters/kotlinx-serialization-json/gradle.properties b/retrofit-converters/kotlinx-serialization-json/gradle.properties new file mode 100644 index 0000000000..df23cbe97e --- /dev/null +++ b/retrofit-converters/kotlinx-serialization-json/gradle.properties @@ -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. diff --git a/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/JsonConverterFactory.kt b/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/JsonConverterFactory.kt new file mode 100644 index 0000000000..dac6f3bbd9 --- /dev/null +++ b/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/JsonConverterFactory.kt @@ -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)) +} diff --git a/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxJsonSerializationConverterFactoryContextualListTest.kt b/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxJsonSerializationConverterFactoryContextualListTest.kt new file mode 100644 index 0000000000..8fbea34d38 --- /dev/null +++ b/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxJsonSerializationConverterFactoryContextualListTest.kt @@ -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> + + @POST("/") + fun serialize(@Body users: List): Call + } + + data class User(val name: String) + + object UserSerializer : KSerializer { + 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"]) + } +} diff --git a/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxSerializationJsonConverterFactoryContextualTest.kt b/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxSerializationJsonConverterFactoryContextualTest.kt new file mode 100644 index 0000000000..4f0311151b --- /dev/null +++ b/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxSerializationJsonConverterFactoryContextualTest.kt @@ -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 + + @POST("/") + fun serialize(@Body user: User): Call + } + + data class User(val name: String) + + object UserSerializer : KSerializer { + 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"]) + } +} diff --git a/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxSerializationJsonConverterFactoryTest.kt b/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxSerializationJsonConverterFactoryTest.kt new file mode 100644 index 0000000000..75ea1364a3 --- /dev/null +++ b/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxSerializationJsonConverterFactoryTest.kt @@ -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 + @POST("/") fun serialize(@Body user: User): Call + } + + @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"]) + } +} diff --git a/retrofit-converters/kotlinx-serialization/src/main/java/retrofit2/converter/kotlinx/serialization/Factory.kt b/retrofit-converters/kotlinx-serialization/src/main/java/retrofit2/converter/kotlinx/serialization/Factory.kt index a4b9275e1b..74b8b9f1b4 100644 --- a/retrofit-converters/kotlinx-serialization/src/main/java/retrofit2/converter/kotlinx/serialization/Factory.kt +++ b/retrofit-converters/kotlinx-serialization/src/main/java/retrofit2/converter/kotlinx/serialization/Factory.kt @@ -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() { diff --git a/retrofit-converters/kotlinx-serialization/src/main/java/retrofit2/converter/kotlinx/serialization/Serializer.kt b/retrofit-converters/kotlinx-serialization/src/main/java/retrofit2/converter/kotlinx/serialization/Serializer.kt index 1c45adc79e..c768dd5151 100644 --- a/retrofit-converters/kotlinx-serialization/src/main/java/retrofit2/converter/kotlinx/serialization/Serializer.kt +++ b/retrofit-converters/kotlinx-serialization/src/main/java/retrofit2/converter/kotlinx/serialization/Serializer.kt @@ -12,7 +12,7 @@ import okhttp3.MediaType import okhttp3.RequestBody import okhttp3.ResponseBody -internal sealed class Serializer { +abstract class Serializer { abstract fun fromResponseBody(loader: DeserializationStrategy, body: ResponseBody): T abstract fun toRequestBody(contentType: MediaType, saver: SerializationStrategy, value: T): RequestBody diff --git a/settings.gradle b/settings.gradle index db95ce9b0c..20fc9aeb1e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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' From 53470be0894c37da9a060cab0f5718b936384613 Mon Sep 17 00:00:00 2001 From: Maksim Kovalev Date: Sat, 8 Jun 2024 19:04:25 +0200 Subject: [PATCH 2/7] Implement Json decoding using streaming API --- .../json/JsonConverterFactory.kt | 5 ++-- .../serialization/json/SerializerFromJson.kt | 24 +++++++++++++++++++ ...ationConverterFactoryContextualListTest.kt | 2 ++ ...ationJsonConverterFactoryContextualTest.kt | 2 ++ ...nxSerializationJsonConverterFactoryTest.kt | 2 ++ 5 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/SerializerFromJson.kt diff --git a/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/JsonConverterFactory.kt b/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/JsonConverterFactory.kt index dac6f3bbd9..1961eeffe8 100644 --- a/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/JsonConverterFactory.kt +++ b/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/JsonConverterFactory.kt @@ -1,10 +1,10 @@ package retrofit2.converter.kotlinx.serialization.json +import kotlinx.serialization.ExperimentalSerializationApi 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. @@ -13,8 +13,9 @@ import retrofit2.converter.kotlinx.serialization.Serializer * 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. */ +@ExperimentalSerializationApi @JvmName("create") fun Json.asConverterFactory(): Converter.Factory { val contentType = MediaType.get("application/json; charset=UTF-8") - return Factory(contentType, Serializer.FromString(this)) + return Factory(contentType, SerializerFromJson(this)) } diff --git a/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/SerializerFromJson.kt b/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/SerializerFromJson.kt new file mode 100644 index 0000000000..05f0caba83 --- /dev/null +++ b/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/SerializerFromJson.kt @@ -0,0 +1,24 @@ +package retrofit2.converter.kotlinx.serialization.json + +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import okhttp3.MediaType +import okhttp3.RequestBody +import okhttp3.ResponseBody +import retrofit2.converter.kotlinx.serialization.Serializer + +@ExperimentalSerializationApi +class SerializerFromJson(override val format: Json) : Serializer() { + override fun fromResponseBody(loader: DeserializationStrategy, body: ResponseBody): T { + val stream = body.byteStream() + return format.decodeFromStream(loader, stream) + } + + override fun toRequestBody(contentType: MediaType, saver: SerializationStrategy, value: T): RequestBody { + val string = format.encodeToString(saver, value) + return RequestBody.create(contentType, string) + } +} diff --git a/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxJsonSerializationConverterFactoryContextualListTest.kt b/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxJsonSerializationConverterFactoryContextualListTest.kt index 8fbea34d38..d47a5fb08f 100644 --- a/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxJsonSerializationConverterFactoryContextualListTest.kt +++ b/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxJsonSerializationConverterFactoryContextualListTest.kt @@ -1,5 +1,6 @@ package retrofit2.converter.kotlinx.serialization.json +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -21,6 +22,7 @@ import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST +@OptIn(ExperimentalSerializationApi::class) class KotlinxJsonSerializationConverterFactoryContextualListTest { @get:Rule val server = MockWebServer() diff --git a/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxSerializationJsonConverterFactoryContextualTest.kt b/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxSerializationJsonConverterFactoryContextualTest.kt index 4f0311151b..be0ce19613 100644 --- a/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxSerializationJsonConverterFactoryContextualTest.kt +++ b/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxSerializationJsonConverterFactoryContextualTest.kt @@ -1,5 +1,6 @@ package retrofit2.converter.kotlinx.serialization.json +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -21,6 +22,7 @@ import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST +@OptIn(ExperimentalSerializationApi::class) class KotlinxSerializationJsonConverterFactoryContextualTest { @get:Rule val server = MockWebServer() diff --git a/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxSerializationJsonConverterFactoryTest.kt b/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxSerializationJsonConverterFactoryTest.kt index 75ea1364a3..d76205b8cc 100644 --- a/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxSerializationJsonConverterFactoryTest.kt +++ b/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxSerializationJsonConverterFactoryTest.kt @@ -1,5 +1,6 @@ package retrofit2.converter.kotlinx.serialization.json +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import okhttp3.mockwebserver.MockResponse @@ -14,6 +15,7 @@ import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST +@OptIn(ExperimentalSerializationApi::class) class KotlinxSerializationJsonConverterFactoryTest { @get:Rule val server = MockWebServer() From 2745415f7d9d6896d52f803cea4b0ec1d4eb1249 Mon Sep 17 00:00:00 2001 From: Maksim Kovalev Date: Sat, 8 Jun 2024 19:09:36 +0200 Subject: [PATCH 3/7] Cleanup dependencies --- retrofit-converters/kotlinx-serialization-json/build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/retrofit-converters/kotlinx-serialization-json/build.gradle b/retrofit-converters/kotlinx-serialization-json/build.gradle index c9f60fdd81..e0474ac578 100644 --- a/retrofit-converters/kotlinx-serialization-json/build.gradle +++ b/retrofit-converters/kotlinx-serialization-json/build.gradle @@ -11,6 +11,4 @@ dependencies { testImplementation libs.junit testImplementation libs.okhttp.mockwebserver - testImplementation libs.kotlinx.serialization.proto - testImplementation libs.kotlinx.serialization.json } From 6443a3bf9d1819c6ea78ab4585432b4aee90f5be Mon Sep 17 00:00:00 2001 From: Maksim Kovalev Date: Mon, 10 Jun 2024 21:21:26 +0200 Subject: [PATCH 4/7] Migrate to kotlinx-serialization-json-okio --- gradle/libs.versions.toml | 1 + retrofit-converters/kotlinx-serialization-json/build.gradle | 2 +- .../kotlinx/serialization/json/SerializerFromJson.kt | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fbd4096590..97c090caf4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,6 +53,7 @@ autoService-compiler = { module = "com.google.auto.service:auto-service", versio kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.8.1" } kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } +kotlinx-serialization-jsonOkio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "kotlinx-serialization" } kotlinx-serialization-proto = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinx-serialization" } okhttp-client = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okhttp-loggingInterceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } diff --git a/retrofit-converters/kotlinx-serialization-json/build.gradle b/retrofit-converters/kotlinx-serialization-json/build.gradle index e0474ac578..852857d037 100644 --- a/retrofit-converters/kotlinx-serialization-json/build.gradle +++ b/retrofit-converters/kotlinx-serialization-json/build.gradle @@ -7,7 +7,7 @@ dependencies { implementation project(':retrofit-converters:kotlinx-serialization') api projects.retrofit - api libs.kotlinx.serialization.json + api libs.kotlinx.serialization.jsonOkio testImplementation libs.junit testImplementation libs.okhttp.mockwebserver diff --git a/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/SerializerFromJson.kt b/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/SerializerFromJson.kt index 05f0caba83..0b86851cf0 100644 --- a/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/SerializerFromJson.kt +++ b/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/SerializerFromJson.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.json.Json -import kotlinx.serialization.json.decodeFromStream +import kotlinx.serialization.json.okio.decodeFromBufferedSource import okhttp3.MediaType import okhttp3.RequestBody import okhttp3.ResponseBody @@ -13,8 +13,8 @@ import retrofit2.converter.kotlinx.serialization.Serializer @ExperimentalSerializationApi class SerializerFromJson(override val format: Json) : Serializer() { override fun fromResponseBody(loader: DeserializationStrategy, body: ResponseBody): T { - val stream = body.byteStream() - return format.decodeFromStream(loader, stream) + val source = body.source() + return format.decodeFromBufferedSource(loader, source) } override fun toRequestBody(contentType: MediaType, saver: SerializationStrategy, value: T): RequestBody { From 422e997ac662dc44207ce8150800907bb50234bf Mon Sep 17 00:00:00 2001 From: Maksim Kovalev Date: Mon, 10 Jun 2024 21:46:44 +0200 Subject: [PATCH 5/7] Create json-specific factory and converters --- .../kotlinx-serialization-json/build.gradle | 2 - .../kotlinx/serialization/json/Factory.kt | 52 +++++++++++++++++++ .../json/JsonConverterFactory.kt | 21 -------- .../json/RequestBodyConverter.kt | 20 +++++++ .../json/ResponseBodyConverter.kt | 20 +++++++ .../serialization/json/SerializerFromJson.kt | 24 --------- .../kotlinx/serialization/Factory.kt | 2 +- .../kotlinx/serialization/Serializer.kt | 2 +- 8 files changed, 94 insertions(+), 49 deletions(-) create mode 100644 retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/Factory.kt delete mode 100644 retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/JsonConverterFactory.kt create mode 100644 retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/RequestBodyConverter.kt create mode 100644 retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/ResponseBodyConverter.kt delete mode 100644 retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/SerializerFromJson.kt diff --git a/retrofit-converters/kotlinx-serialization-json/build.gradle b/retrofit-converters/kotlinx-serialization-json/build.gradle index 852857d037..959199d0d5 100644 --- a/retrofit-converters/kotlinx-serialization-json/build.gradle +++ b/retrofit-converters/kotlinx-serialization-json/build.gradle @@ -4,8 +4,6 @@ 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.jsonOkio diff --git a/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/Factory.kt b/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/Factory.kt new file mode 100644 index 0000000000..d75e00d7a0 --- /dev/null +++ b/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/Factory.kt @@ -0,0 +1,52 @@ +package retrofit2.converter.kotlinx.serialization.json + +import java.lang.reflect.Type +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer +import okhttp3.RequestBody +import okhttp3.ResponseBody +import retrofit2.Converter +import retrofit2.Retrofit + +@ExperimentalSerializationApi +internal class Factory( + private val json: Json, +) : Converter.Factory() { + + @Suppress("RedundantNullableReturnType") // Retaining interface contract. + override fun responseBodyConverter( + type: Type, + annotations: Array, + retrofit: Retrofit, + ): Converter? { + val loader = serializer(type) + return ResponseBodyConverter(json, loader) + } + + @Suppress("RedundantNullableReturnType") // Retaining interface contract. + override fun requestBodyConverter( + type: Type, + parameterAnnotations: Array, + methodAnnotations: Array, + retrofit: Retrofit, + ): Converter<*, RequestBody>? { + val saver = serializer(type) + return RequestBodyConverter(json, saver) + } + + private fun serializer(type: Type) = json.serializersModule.serializer(type) +} + +/** + * 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. + */ +@ExperimentalSerializationApi +@JvmName("create") +fun Json.asConverterFactory(): Converter.Factory { + return Factory(this) +} diff --git a/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/JsonConverterFactory.kt b/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/JsonConverterFactory.kt deleted file mode 100644 index 1961eeffe8..0000000000 --- a/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/JsonConverterFactory.kt +++ /dev/null @@ -1,21 +0,0 @@ -package retrofit2.converter.kotlinx.serialization.json - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.Json -import okhttp3.MediaType -import retrofit2.Converter -import retrofit2.converter.kotlinx.serialization.Factory - -/** - * 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. - */ -@ExperimentalSerializationApi -@JvmName("create") -fun Json.asConverterFactory(): Converter.Factory { - val contentType = MediaType.get("application/json; charset=UTF-8") - return Factory(contentType, SerializerFromJson(this)) -} diff --git a/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/RequestBodyConverter.kt b/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/RequestBodyConverter.kt new file mode 100644 index 0000000000..f42bc17339 --- /dev/null +++ b/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/RequestBodyConverter.kt @@ -0,0 +1,20 @@ +package retrofit2.converter.kotlinx.serialization.json + +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.json.Json +import okhttp3.MediaType +import okhttp3.RequestBody +import retrofit2.Converter + +internal class RequestBodyConverter( + private val json: Json, + private val saver: SerializationStrategy, +) : Converter { + + private val contentType = MediaType.get("application/json; charset=UTF-8") + + override fun convert(value: T): RequestBody { + val string = json.encodeToString(saver, value) + return RequestBody.create(contentType, string) + } +} diff --git a/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/ResponseBodyConverter.kt b/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/ResponseBodyConverter.kt new file mode 100644 index 0000000000..137760e859 --- /dev/null +++ b/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/ResponseBodyConverter.kt @@ -0,0 +1,20 @@ +package retrofit2.converter.kotlinx.serialization.json + +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.okio.decodeFromBufferedSource +import okhttp3.ResponseBody +import retrofit2.Converter + +@ExperimentalSerializationApi +class ResponseBodyConverter( + private val json: Json, + private val loader: DeserializationStrategy, +) : Converter { + + override fun convert(value: ResponseBody): T? { + val source = value.source() + return json.decodeFromBufferedSource(loader, source) + } +} diff --git a/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/SerializerFromJson.kt b/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/SerializerFromJson.kt deleted file mode 100644 index 0b86851cf0..0000000000 --- a/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/SerializerFromJson.kt +++ /dev/null @@ -1,24 +0,0 @@ -package retrofit2.converter.kotlinx.serialization.json - -import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationStrategy -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.okio.decodeFromBufferedSource -import okhttp3.MediaType -import okhttp3.RequestBody -import okhttp3.ResponseBody -import retrofit2.converter.kotlinx.serialization.Serializer - -@ExperimentalSerializationApi -class SerializerFromJson(override val format: Json) : Serializer() { - override fun fromResponseBody(loader: DeserializationStrategy, body: ResponseBody): T { - val source = body.source() - return format.decodeFromBufferedSource(loader, source) - } - - override fun toRequestBody(contentType: MediaType, saver: SerializationStrategy, value: T): RequestBody { - val string = format.encodeToString(saver, value) - return RequestBody.create(contentType, string) - } -} diff --git a/retrofit-converters/kotlinx-serialization/src/main/java/retrofit2/converter/kotlinx/serialization/Factory.kt b/retrofit-converters/kotlinx-serialization/src/main/java/retrofit2/converter/kotlinx/serialization/Factory.kt index 74b8b9f1b4..a4b9275e1b 100644 --- a/retrofit-converters/kotlinx-serialization/src/main/java/retrofit2/converter/kotlinx/serialization/Factory.kt +++ b/retrofit-converters/kotlinx-serialization/src/main/java/retrofit2/converter/kotlinx/serialization/Factory.kt @@ -13,7 +13,7 @@ import okhttp3.ResponseBody import retrofit2.Converter import retrofit2.Retrofit -class Factory( +internal class Factory( private val contentType: MediaType, private val serializer: Serializer ) : Converter.Factory() { diff --git a/retrofit-converters/kotlinx-serialization/src/main/java/retrofit2/converter/kotlinx/serialization/Serializer.kt b/retrofit-converters/kotlinx-serialization/src/main/java/retrofit2/converter/kotlinx/serialization/Serializer.kt index c768dd5151..1c45adc79e 100644 --- a/retrofit-converters/kotlinx-serialization/src/main/java/retrofit2/converter/kotlinx/serialization/Serializer.kt +++ b/retrofit-converters/kotlinx-serialization/src/main/java/retrofit2/converter/kotlinx/serialization/Serializer.kt @@ -12,7 +12,7 @@ import okhttp3.MediaType import okhttp3.RequestBody import okhttp3.ResponseBody -abstract class Serializer { +internal sealed class Serializer { abstract fun fromResponseBody(loader: DeserializationStrategy, body: ResponseBody): T abstract fun toRequestBody(contentType: MediaType, saver: SerializationStrategy, value: T): RequestBody From 0941c8180fecd0711a3a217ba0bed0547691b6a6 Mon Sep 17 00:00:00 2001 From: Maksim Kovalev Date: Mon, 10 Jun 2024 21:46:56 +0200 Subject: [PATCH 6/7] Fix test name --- ...tlinxSerializationJsonConverterFactoryContextualListTest.kt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/{KotlinxJsonSerializationConverterFactoryContextualListTest.kt => KotlinxSerializationJsonConverterFactoryContextualListTest.kt} (97%) diff --git a/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxJsonSerializationConverterFactoryContextualListTest.kt b/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxSerializationJsonConverterFactoryContextualListTest.kt similarity index 97% rename from retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxJsonSerializationConverterFactoryContextualListTest.kt rename to retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxSerializationJsonConverterFactoryContextualListTest.kt index d47a5fb08f..4a2641d33f 100644 --- a/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxJsonSerializationConverterFactoryContextualListTest.kt +++ b/retrofit-converters/kotlinx-serialization-json/src/test/java/retrofit2/converter/kotlinx/serialization/json/KotlinxSerializationJsonConverterFactoryContextualListTest.kt @@ -23,7 +23,7 @@ import retrofit2.http.GET import retrofit2.http.POST @OptIn(ExperimentalSerializationApi::class) -class KotlinxJsonSerializationConverterFactoryContextualListTest { +class KotlinxSerializationJsonConverterFactoryContextualListTest { @get:Rule val server = MockWebServer() From 9a7c365f8fe1d95a690b251953254eac9180b608 Mon Sep 17 00:00:00 2001 From: Maksim Kovalev Date: Mon, 10 Jun 2024 21:50:25 +0200 Subject: [PATCH 7/7] Remove redundant nullability --- .../kotlinx/serialization/json/ResponseBodyConverter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/ResponseBodyConverter.kt b/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/ResponseBodyConverter.kt index 137760e859..d5f8cd0972 100644 --- a/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/ResponseBodyConverter.kt +++ b/retrofit-converters/kotlinx-serialization-json/src/main/java/retrofit2/converter/kotlinx/serialization/json/ResponseBodyConverter.kt @@ -13,7 +13,7 @@ class ResponseBodyConverter( private val loader: DeserializationStrategy, ) : Converter { - override fun convert(value: ResponseBody): T? { + override fun convert(value: ResponseBody): T { val source = value.source() return json.decodeFromBufferedSource(loader, source) }