diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 844671c5..f145bf05 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -16,6 +16,7 @@ plugins { kotlin("multiplatform") + kotlin("plugin.serialization") id("com.android.library") alias(libs.plugins.ksp) id("maven-publish") @@ -30,6 +31,7 @@ kotlin { dependencies { implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.serialization) } } val commonTest by getting { diff --git a/core/src/androidUnitTest/kotlin/com/google/adk/kt/JsonSerializationFallbackAndroidTest.kt b/core/src/androidUnitTest/kotlin/com/google/adk/kt/JsonSerializationFallbackAndroidTest.kt new file mode 100644 index 00000000..aeae1dab --- /dev/null +++ b/core/src/androidUnitTest/kotlin/com/google/adk/kt/JsonSerializationFallbackAndroidTest.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.kt + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.adk.kt.serialization.Json +import kotlin.test.assertEquals +import kotlinx.serialization.json.JsonPrimitive +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Android behavior of the non-`@Serializable` fallback in `Json.serializeToJsonElement` (org.json). + */ +@RunWith(AndroidJUnit4::class) +class JsonSerializationFallbackAndroidTest { + + // Not `@Serializable`, so the platform fallback (org.json on Android) is used. + private data class Unannotated(val v: Int) + + @Test + fun nonSerializableType_stringifiesViaOrgJson() { + // org.json cannot reflect, so the value is stringified via `toString()`. + assertEquals(JsonPrimitive("Unannotated(v=7)"), Json.serializeToJsonElement(Unannotated(7))) + } +} diff --git a/core/src/commonMain/kotlin/com/google/adk/kt/serialization/Json.kt b/core/src/commonMain/kotlin/com/google/adk/kt/serialization/Json.kt index 15968696..36690c3b 100644 --- a/core/src/commonMain/kotlin/com/google/adk/kt/serialization/Json.kt +++ b/core/src/commonMain/kotlin/com/google/adk/kt/serialization/Json.kt @@ -16,6 +16,11 @@ package com.google.adk.kt.serialization +import kotlin.reflect.KClass +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.serializerOrNull + /** Platform-independent utility for JSON serialization. */ interface Json { /** Serializes an object to a JSON string. */ @@ -24,7 +29,23 @@ interface Json { /** Parses a JSON string to a map. */ fun fromJsonToMap(json: String): Map - companion object : Json by getJson() + companion object : Json by getJson() { + private val KMP_JSON = kotlinx.serialization.json.Json { explicitNulls = false } + + /** + * Converts [value] to a [JsonElement]: kotlinx.serialization for `@Serializable` types, + * otherwise the platform [Json] fallback (best-effort, not round-trippable). + */ + @OptIn(InternalSerializationApi::class) + internal fun serializeToJsonElement(value: Any): JsonElement { + @Suppress("UNCHECKED_CAST") val serializer = (value::class as KClass).serializerOrNull() + return if (serializer != null) { + KMP_JSON.encodeToJsonElement(serializer, value) + } else { + KMP_JSON.parseToJsonElement(toJsonString(value)) + } + } + } } internal expect fun getJson(): Json diff --git a/core/src/commonTest/kotlin/com/google/adk/kt/serialization/SerializeToJsonElementTest.kt b/core/src/commonTest/kotlin/com/google/adk/kt/serialization/SerializeToJsonElementTest.kt new file mode 100644 index 00000000..d6244b89 --- /dev/null +++ b/core/src/commonTest/kotlin/com/google/adk/kt/serialization/SerializeToJsonElementTest.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.kt.serialization + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json as KotlinxJson + +class SerializeToJsonElementTest { + + @Serializable private data class Point(val x: Int, val y: Int, val label: String? = null) + + @Test + fun serializableType_emitsJsonObject() { + // `@Serializable` -> structured JSON object via kotlinx.serialization (JVM and Android). + assertEquals( + KotlinxJson.parseToJsonElement("""{"x":1,"y":2}"""), + Json.serializeToJsonElement(Point(1, 2)), + ) + } + + @Test + fun serializableType_omitsNullProperties() { + // `explicitNulls = false`: a null property (`label`) is dropped from the encoded object. + assertEquals( + KotlinxJson.parseToJsonElement("""{"x":1,"y":2}"""), + Json.serializeToJsonElement(Point(1, 2, label = null)), + ) + } +} diff --git a/core/src/jvmTest/kotlin/com/google/adk/kt/JsonSerializationFallbackJvmTest.kt b/core/src/jvmTest/kotlin/com/google/adk/kt/JsonSerializationFallbackJvmTest.kt new file mode 100644 index 00000000..9bf424e2 --- /dev/null +++ b/core/src/jvmTest/kotlin/com/google/adk/kt/JsonSerializationFallbackJvmTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.kt + +import com.google.adk.kt.serialization.Json +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlinx.serialization.json.Json as KotlinxJson + +/** JVM behavior of the non-`@Serializable` fallback in `Json.serializeToJsonElement` (Gson). */ +class JsonSerializationFallbackJvmTest { + + // Not `@Serializable`, so the platform fallback (Gson on JVM) is used. + private data class Unannotated(val v: Int) + + @Test + fun nonSerializableType_reflectsToJsonObjectViaGson() { + // On JVM the Gson-backed fallback reflects the data class into a structured JSON object. + assertEquals( + KotlinxJson.parseToJsonElement("""{"v":7}"""), + Json.serializeToJsonElement(Unannotated(7)), + ) + } +}