diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 69ac663..68a7b59 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -30,6 +30,7 @@
+
diff --git a/dachlatten-retrofit/build.gradle.kts b/dachlatten-retrofit/build.gradle.kts
new file mode 100644
index 0000000..ba3a813
--- /dev/null
+++ b/dachlatten-retrofit/build.gradle.kts
@@ -0,0 +1,15 @@
+plugins {
+ id("android-library-base")
+ id("android-library-unit-test")
+ id("android-library-release")
+ alias(libs.plugins.kotlin.serialization.plugin)
+}
+
+dependencies {
+ implementation(libs.kotlinx.serialization)
+ implementation(libs.retrofit.serialization)
+ implementation(libs.okhttp)
+
+ testImplementation(libs.kotlinx.serialization)
+ testImplementation(libs.okhttp.mockwebserver)
+}
diff --git a/dachlatten-retrofit/src/main/kotlin/de/sipgate/dachlatten/okhttp/StreamingJsonConverterFactory.kt b/dachlatten-retrofit/src/main/kotlin/de/sipgate/dachlatten/okhttp/StreamingJsonConverterFactory.kt
new file mode 100644
index 0000000..e4dc1b5
--- /dev/null
+++ b/dachlatten-retrofit/src/main/kotlin/de/sipgate/dachlatten/okhttp/StreamingJsonConverterFactory.kt
@@ -0,0 +1,50 @@
+package de.sipgate.dachlatten.okhttp
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.decodeFromStream
+import kotlinx.serialization.json.encodeToStream
+import kotlinx.serialization.serializer
+import okhttp3.MediaType
+import okhttp3.RequestBody
+import okhttp3.RequestBody.Companion.toRequestBody
+import okhttp3.ResponseBody
+import retrofit2.Converter
+import retrofit2.Retrofit
+import java.io.ByteArrayOutputStream
+import java.lang.reflect.Type
+
+@JvmName("convert")
+@ExperimentalSerializationApi
+fun Json.asConverterFactory(contentType: MediaType): Converter.Factory =
+ object : Converter.Factory() {
+ override fun responseBodyConverter(
+ type: Type,
+ annotations: Array,
+ retrofit: Retrofit,
+ ) = Converter { value ->
+ this@asConverterFactory.decodeFromStream(
+ this@asConverterFactory.serializersModule.serializer(type),
+ value.byteStream(),
+ )
+ }
+
+ override fun requestBodyConverter(
+ type: Type,
+ parameterAnnotations: Array,
+ methodAnnotations: Array,
+ retrofit: Retrofit,
+ ) = Converter { value ->
+ val stream = ByteArrayOutputStream()
+ this@asConverterFactory.encodeToStream(
+ this@asConverterFactory.serializersModule.serializer(type),
+ value,
+ stream,
+ )
+ stream.toByteArray().toRequestBody(
+ contentType,
+ 0,
+ stream.size(),
+ )
+ }
+ }
diff --git a/dachlatten-retrofit/src/test/kotlin/de/sipgate/dachlatten/okhttp/KotlinSerializationConverterFactoryStringTest.kt b/dachlatten-retrofit/src/test/kotlin/de/sipgate/dachlatten/okhttp/KotlinSerializationConverterFactoryStringTest.kt
new file mode 100644
index 0000000..d5b0a74
--- /dev/null
+++ b/dachlatten-retrofit/src/test/kotlin/de/sipgate/dachlatten/okhttp/KotlinSerializationConverterFactoryStringTest.kt
@@ -0,0 +1,60 @@
+package de.sipgate.dachlatten.okhttp
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import mockwebserver3.MockResponse
+import mockwebserver3.MockWebServer
+import mockwebserver3.junit5.internal.MockWebServerExtension
+import okhttp3.MediaType.Companion.toMediaType
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import retrofit2.Call
+import retrofit2.Retrofit
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.POST
+
+@OptIn(ExperimentalSerializationApi::class)
+@ExtendWith(MockWebServerExtension::class)
+class KotlinSerializationConverterFactoryStringTest {
+ 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)
+
+ @BeforeEach
+ fun setUp(server: MockWebServer) {
+ val contentType = "application/json; charset=utf-8".toMediaType()
+ val retrofit = Retrofit.Builder()
+ .baseUrl(server.url("/"))
+ .addConverterFactory(Json.asConverterFactory(contentType))
+ .build()
+ service = retrofit.create(Service::class.java)
+ }
+
+ @Test
+ fun deserialize(server: MockWebServer) {
+ server.enqueue(MockResponse().newBuilder().body("""{"name":"Bob"}""").build())
+ val user = service.deserialize().execute().body()!!
+ assertEquals(User("Bob"), user)
+ }
+
+ @Test
+ fun serialize(server: MockWebServer) {
+ 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/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f765183..f3d46db 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -12,9 +12,12 @@ robolectric = "4.10.3"
compose = "1.5.4"
compose-compiler = "1.5.7"
kotlinx-datetime = "0.5.0"
+kotlinx-serialization = "1.6.2"
kover = "0.7.5"
mockk = "1.13.9"
androidx-activity = "1.8.2"
+okhttp = "4.12.0"
+retrofit-serialization = "1.0.0"
[libraries]
coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
@@ -35,15 +38,20 @@ kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-pl
kover-gradlePlugin = { group = "org.jetbrains.kotlinx.kover", name = "org.jetbrains.kotlinx.kover.gradle.plugin", version.ref = "kover" }
annotation-jvm = { group = "androidx.annotation", name = "annotation-jvm", version.ref = "annotation-jvm" }
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime-jvm", version.ref = "kotlinx-datetime" }
+kotlinx-serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
+okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
+okhttp-mockwebserver = { group = "com.squareup.okhttp3", name = "mockwebserver3-junit5", version.prefer = "5.0.0-alpha.12" }
+retrofit-serialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofit-serialization" }
mockk-agent = { group = "io.mockk", name = "mockk-agent", version.ref = "mockk" }
mockk-jvm = { group = "io.mockk", name = "mockk-jvm", version.ref = "mockk" }
[bundles]
compose-ui-test = ["compose-ui-test-junit4", "compose-ui-test-manifest"]
-mockk = ["mockk-agent", "mockk-jvm"]
+mockk = ["mockk-agent", "mockk-jvm"]
[plugins]
androidLibrary = { id = "com.android.library", version.ref = "agp" }
kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
koverPlugin = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
+kotlin-serialization-plugin = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 4ebe18a..2743031 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -28,3 +28,4 @@ include(":dachlatten-debug")
include(":dachlatten-flow")
include(":dachlatten-google")
include(":dachlatten-primitives")
+include(":dachlatten-retrofit")