Skip to content

Commit d809d4e

Browse files
authored
Migrate from Klaxon decoding to kotlinx-serialization (#851)
* Migrate from Klaxon decoding to kotlinx-serialization * Remove klaxon
1 parent 54fadd1 commit d809d4e

File tree

9 files changed

+190
-101
lines changed

9 files changed

+190
-101
lines changed

.changeset/shy-foxes-brake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"client-sdk-android": patch
3+
---
4+
5+
Migrate from Klaxon decoding to kotlinx-serialization for AgentAttribute deserialization

gradle/libs.versions.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ groupie = "2.9.0"
1515
junit-lib = "4.13.2"
1616
junit-jupiter = "5.5.0"
1717
jwtdecode = "2.0.2"
18-
klaxon = "5.5"
1918
kotlinx-serialization = "1.5.0"
2019
leakcanaryAndroid = "2.8.1"
2120
lint = "30.0.1"
@@ -51,8 +50,6 @@ dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref =
5150
groupie = { module = "com.github.lisawray.groupie:groupie", version.ref = "groupie" }
5251
groupie-viewbinding = { module = "com.github.lisawray.groupie:groupie-viewbinding", version.ref = "groupie" }
5352
jwtdecode = { module = "com.auth0.android:jwtdecode", version.ref = "jwtdecode" }
54-
klaxon = { module = "com.beust:klaxon", version.ref = "klaxon" }
55-
noise = { module = "com.github.paramsen:noise", version.ref = "noise" }
5653
androidx-lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidx-lifecycle" }
5754
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }
5855
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "androidx-lifecycle" }

livekit-android-sdk/build.gradle

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ dependencies {
148148
api libs.okhttp.lib
149149
implementation libs.okhttp.coroutines
150150
api libs.audioswitch
151-
implementation libs.klaxon
152151
implementation libs.jwtdecode
153152

154153
implementation libs.androidx.annotation

livekit-android-sdk/consumer-rules.pro

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,3 @@
3838
# Protobuf
3939
#########################################
4040
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
41-
42-
# Klaxon JSON parsing
43-
#########################################
44-
# Klaxon uses reflection and doesn't ship ProGuard rules.
45-
# Keep Klaxon library classes for reflection to work
46-
-keep class com.beust.klaxon.** { *; }
47-
-keep interface com.beust.klaxon.** { *; }
48-
# Data classes using Klaxon should be annotated with @Keep at the source level

livekit-android-sdk/src/main/java/io/livekit/android/room/types/AgentTypes.kt

Lines changed: 94 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -16,61 +16,50 @@
1616

1717
package io.livekit.android.room.types
1818

19-
import android.annotation.SuppressLint
2019
import androidx.annotation.Keep
21-
import com.beust.klaxon.Converter
22-
import com.beust.klaxon.Json
23-
import com.beust.klaxon.JsonValue
24-
import com.beust.klaxon.Klaxon
2520
import io.livekit.android.util.LKLog
21+
import kotlinx.serialization.KSerializer
22+
import kotlinx.serialization.SerialName
2623
import kotlinx.serialization.Serializable
27-
28-
private fun <T> Klaxon.convert(k: kotlin.reflect.KClass<*>, fromJson: (JsonValue) -> T, toJson: (T) -> String, isUnion: Boolean = false) =
29-
this.converter(
30-
object : Converter {
31-
@Suppress("UNCHECKED_CAST")
32-
override fun toJson(value: Any) = toJson(value as T)
33-
override fun fromJson(jv: JsonValue) = fromJson(jv) as Any?
34-
override fun canConvert(cls: Class<*>) = cls == k.java || (isUnion && cls.superclass == k.java)
35-
},
36-
)
37-
38-
internal val klaxon = Klaxon()
39-
.convert(AgentInput::class, { it.string?.let { AgentInput.fromValue(it) } }, { "\"${it?.value}\"" })
40-
.convert(AgentOutput::class, { it.string?.let { AgentOutput.fromValue(it) } }, { "\"${it?.value}\"" })
41-
.convert(AgentSdkState::class, { it.string?.let { AgentSdkState.fromValue(it) } }, { "\"${it?.value}\"" })
24+
import kotlinx.serialization.descriptors.PrimitiveKind
25+
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
26+
import kotlinx.serialization.descriptors.SerialDescriptor
27+
import kotlinx.serialization.encoding.Decoder
28+
import kotlinx.serialization.encoding.Encoder
4229

4330
@Keep
31+
@Serializable
4432
data class AgentAttributes(
45-
@Json(name = "lk.agent.inputs")
33+
@SerialName("lk.agent.inputs")
4634
val lkAgentInputs: List<AgentInput>? = null,
4735

48-
@Json(name = "lk.agent.outputs")
36+
@SerialName("lk.agent.outputs")
4937
val lkAgentOutputs: List<AgentOutput>? = null,
5038

51-
@Json(name = "lk.agent.state")
39+
@SerialName("lk.agent.state")
5240
val lkAgentState: AgentSdkState? = null,
5341

54-
@Json(name = "lk.publish_on_behalf")
42+
@SerialName("lk.publish_on_behalf")
5543
val lkPublishOnBehalf: String? = null,
56-
) {
57-
fun toJson() = klaxon.toJsonString(this)
58-
59-
companion object {
60-
fun fromJson(json: String) = klaxon.parse<AgentAttributes>(json)
61-
}
62-
}
44+
)
6345

6446
@Keep
47+
@Serializable(with = AgentInputSerializer::class)
6548
enum class AgentInput(val value: String) {
49+
@SerialName("audio")
6650
Audio("audio"),
51+
52+
@SerialName("text")
6753
Text("text"),
54+
55+
@SerialName("video")
6856
Video("video"),
69-
Unknown("unknown"),
70-
;
57+
58+
@SerialName("unknown")
59+
Unknown("unknown");
7160

7261
companion object {
73-
fun fromValue(value: String): AgentInput? = when (value) {
62+
fun fromValue(value: String): AgentInput = when (value) {
7463
"audio" -> Audio
7564
"text" -> Text
7665
"video" -> Video
@@ -83,14 +72,34 @@ enum class AgentInput(val value: String) {
8372
}
8473

8574
@Keep
75+
internal object AgentInputSerializer : KSerializer<AgentInput> {
76+
// Serial names of descriptors should be unique, this is why we advise including app package in the name.
77+
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("io.livekit.android.room.types.AgentInput", PrimitiveKind.STRING)
78+
79+
override fun serialize(encoder: Encoder, value: AgentInput) {
80+
encoder.encodeString(value.value)
81+
}
82+
83+
override fun deserialize(decoder: Decoder): AgentInput {
84+
val string = decoder.decodeString()
85+
return AgentInput.fromValue(string)
86+
}
87+
}
88+
89+
@Keep
90+
@Serializable(with = AgentOutputSerializer::class)
8691
enum class AgentOutput(val value: String) {
92+
@SerialName("audio")
8793
Audio("audio"),
94+
95+
@SerialName("transcription")
8896
Transcription("transcription"),
89-
Unknown("unknown"),
90-
;
97+
98+
@SerialName("unknown")
99+
Unknown("unknown");
91100

92101
companion object {
93-
fun fromValue(value: String): AgentOutput? = when (value) {
102+
fun fromValue(value: String): AgentOutput = when (value) {
94103
"audio" -> Audio
95104
"transcription" -> Transcription
96105
else -> {
@@ -101,19 +110,45 @@ enum class AgentOutput(val value: String) {
101110
}
102111
}
103112

113+
@Keep
114+
internal object AgentOutputSerializer : KSerializer<AgentOutput> {
115+
// Serial names of descriptors should be unique, this is why we advise including app package in the name.
116+
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("io.livekit.android.room.types.AgentOutput", PrimitiveKind.STRING)
117+
118+
override fun serialize(encoder: Encoder, value: AgentOutput) {
119+
encoder.encodeString(value.value)
120+
}
121+
122+
override fun deserialize(decoder: Decoder): AgentOutput {
123+
val string = decoder.decodeString()
124+
return AgentOutput.fromValue(string)
125+
}
126+
}
127+
104128
// Renamed from AgentState to AgentSdkState to avoid naming conflicts elsewhere.
105129
@Keep
130+
@Serializable(with = AgentSdkStateSerializer::class)
106131
enum class AgentSdkState(val value: String) {
132+
@SerialName("idle")
107133
Idle("idle"),
134+
135+
@SerialName("initializing")
108136
Initializing("initializing"),
137+
138+
@SerialName("listening")
109139
Listening("listening"),
140+
141+
@SerialName("speaking")
110142
Speaking("speaking"),
143+
144+
@SerialName("thinking")
111145
Thinking("thinking"),
112-
Unknown("unknown"),
113-
;
146+
147+
@SerialName("unknown")
148+
Unknown("unknown");
114149

115150
companion object {
116-
fun fromValue(value: String): AgentSdkState? = when (value) {
151+
fun fromValue(value: String): AgentSdkState = when (value) {
117152
"idle" -> Idle
118153
"initializing" -> Initializing
119154
"listening" -> Listening
@@ -127,34 +162,42 @@ enum class AgentSdkState(val value: String) {
127162
}
128163
}
129164

165+
@Keep
166+
internal object AgentSdkStateSerializer : KSerializer<AgentSdkState> {
167+
// Serial names of descriptors should be unique, this is why we advise including app package in the name.
168+
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("io.livekit.android.room.types.AgentSdkState", PrimitiveKind.STRING)
169+
170+
override fun serialize(encoder: Encoder, value: AgentSdkState) {
171+
encoder.encodeString(value.value)
172+
}
173+
174+
override fun deserialize(decoder: Decoder): AgentSdkState {
175+
val string = decoder.decodeString()
176+
return AgentSdkState.fromValue(string)
177+
}
178+
}
179+
130180
/**
131181
* Schema for transcription-related attributes
132182
*/
133183
@Keep
134-
@SuppressLint("UnsafeOptInUsageError")
135184
@Serializable
136185
data class TranscriptionAttributes(
137186
/**
138187
* The segment id of the transcription
139188
*/
140-
@Json(name = "lk.segment_id")
189+
@SerialName("lk.segment_id")
141190
val lkSegmentID: String? = null,
142191

143192
/**
144193
* The associated track id of the transcription
145194
*/
146-
@Json(name = "lk.transcribed_track_id")
195+
@SerialName("lk.transcribed_track_id")
147196
val lkTranscribedTrackID: String? = null,
148197

149198
/**
150199
* Whether the transcription is final
151200
*/
152-
@Json(name = "lk.transcription_final")
201+
@SerialName("lk.transcription_final")
153202
val lkTranscriptionFinal: Boolean? = null,
154-
) {
155-
fun toJson() = klaxon.toJsonString(this)
156-
157-
companion object {
158-
fun fromJson(json: String) = klaxon.parse<TranscriptionAttributes>(json)
159-
}
160-
}
203+
)

livekit-android-sdk/src/main/java/io/livekit/android/room/types/AgentTypesExt.kt

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,44 @@
1616

1717
package io.livekit.android.room.types
1818

19-
import com.beust.klaxon.JsonObject
19+
import androidx.annotation.VisibleForTesting
20+
import kotlinx.serialization.decodeFromString
21+
import kotlinx.serialization.json.Json
22+
import kotlinx.serialization.json.JsonArray
23+
import kotlinx.serialization.json.JsonElement
24+
import kotlinx.serialization.json.JsonObject
25+
import kotlinx.serialization.json.JsonPrimitive
26+
import kotlinx.serialization.json.decodeFromJsonElement
2027

2128
// AgentTypes.kt is a generated file and should not be edited.
2229
// Add any required functions through extensions here.
23-
30+
private val jsonSerializer = Json {
31+
allowStructuredMapKeys = true
32+
coerceInputValues = true
33+
}
2434
internal fun AgentAttributes.Companion.fromJsonObject(jsonObject: JsonObject) =
25-
klaxon.parseFromJsonObject<AgentAttributes>(jsonObject)
35+
jsonSerializer.decodeFromJsonElement<AgentAttributes>(jsonObject)
2636

2737
/**
2838
* @suppress
2939
*/
30-
fun AgentAttributes.Companion.fromMap(map: Map<String, *>): AgentAttributes {
31-
if (map.values.all { it == null }) {
40+
fun AgentAttributes.Companion.fromMap(map: Map<String, JsonElement>): AgentAttributes {
41+
if (map.values.none()) {
3242
return AgentAttributes()
3343
}
3444

35-
return fromJsonObject(JsonObject(map)) ?: AgentAttributes()
45+
return fromJsonObject(JsonObject(map))
3646
}
3747

3848
/**
3949
* @suppress
4050
*/
4151
fun AgentAttributes.Companion.fromStringMap(map: Map<String, String>): AgentAttributes {
42-
val parseMap = mutableMapOf<String, Any?>()
52+
val parseMap = mutableMapOf<String, JsonElement>()
4353
for ((key, converter) in AGENT_ATTRIBUTES_CONVERSION) {
44-
parseMap[key] = converter(map[key])
54+
converter(map[key])?.let { converted ->
55+
parseMap[key] = converted
56+
}
4557
}
4658

4759
return fromMap(parseMap)
@@ -51,40 +63,49 @@ fun AgentAttributes.Companion.fromStringMap(map: Map<String, String>): AgentAttr
5163
* Protobuf attribute maps are [String, String], so need to parse arrays/maps manually.
5264
* @suppress
5365
*/
54-
val AGENT_ATTRIBUTES_CONVERSION = mapOf<String, (String?) -> Any?>(
55-
"lk.agent.inputs" to { json -> json?.let { klaxon.parseArray<List<String>>(json) } },
56-
"lk.agent.outputs" to { json -> json?.let { klaxon.parseArray<List<String>>(json) } },
57-
"lk.agent.state" to { json -> json },
58-
"lk.publish_on_behalf" to { json -> json },
66+
@VisibleForTesting
67+
val AGENT_ATTRIBUTES_CONVERSION = mapOf<String, (String?) -> JsonElement?>(
68+
"lk.agent.inputs" to { json -> json?.let { jsonSerializer.decodeFromString<JsonArray>(json) } },
69+
"lk.agent.outputs" to { json -> json?.let { jsonSerializer.decodeFromString<JsonArray>(json) } },
70+
"lk.agent.state" to { json -> JsonPrimitive(json) },
71+
"lk.publish_on_behalf" to { json -> JsonPrimitive(json) },
5972
)
6073

6174
internal fun TranscriptionAttributes.Companion.fromJsonObject(jsonObject: JsonObject) =
62-
klaxon.parseFromJsonObject<TranscriptionAttributes>(jsonObject)
75+
jsonSerializer.decodeFromJsonElement<TranscriptionAttributes>(jsonObject)
6376

6477
/**
6578
* @suppress
6679
*/
67-
fun TranscriptionAttributes.Companion.fromMap(map: Map<String, *>): TranscriptionAttributes {
68-
return fromJsonObject(JsonObject(map)) ?: TranscriptionAttributes()
80+
fun TranscriptionAttributes.Companion.fromMap(map: Map<String, JsonElement>): TranscriptionAttributes {
81+
if (map.values.none()) {
82+
return TranscriptionAttributes()
83+
}
84+
85+
return fromJsonObject(JsonObject(map))
6986
}
7087

7188
/**
7289
* @suppress
7390
*/
7491
fun TranscriptionAttributes.Companion.fromStringMap(map: Map<String, String>): TranscriptionAttributes {
75-
val parseMap = mutableMapOf<String, Any?>()
92+
val parseMap = mutableMapOf<String, JsonElement>()
7693
for ((key, converter) in TRANSCRIPTION_ATTRIBUTES_CONVERSION) {
77-
parseMap[key] = converter(map[key])
94+
converter(map[key])?.let { converted ->
95+
parseMap[key] = converted
96+
}
7897
}
98+
7999
return fromMap(parseMap)
80100
}
81101

82102
/**
83103
* Protobuf attribute maps are [String, String], so need to parse arrays/maps manually.
84104
* @suppress
85105
*/
86-
val TRANSCRIPTION_ATTRIBUTES_CONVERSION = mapOf<String, (String?) -> Any?>(
87-
"lk.segment_id" to { json -> json },
88-
"lk.transcribed_track_id" to { json -> json },
89-
"lk.transcription_final" to { json -> json?.let { klaxon.parse(json) } },
106+
@VisibleForTesting
107+
val TRANSCRIPTION_ATTRIBUTES_CONVERSION = mapOf<String, (String?) -> JsonElement?>(
108+
"lk.segment_id" to { json -> JsonPrimitive(json) },
109+
"lk.transcribed_track_id" to { json -> JsonPrimitive(json) },
110+
"lk.transcription_final" to { json -> json?.let { jsonSerializer.decodeFromString<JsonArray>(json) } },
90111
)

livekit-android-test/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ dependencies {
9797
testImplementation libs.junit
9898
testImplementation libs.robolectric
9999
testImplementation libs.okhttp.mockwebserver
100-
testImplementation libs.klaxon
100+
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
101101
kaptTest libs.dagger.compiler
102102

103103
androidTestImplementation libs.androidx.test.junit

0 commit comments

Comments
 (0)