I am trying to write custom serializer, because I need to deserialize object into string. Deserialization of object is done correctly, but when I try to deserialize simple String I got exception:
kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 10: Expected quotation mark '"', but had 't' instead at path: $.plan
JSON input: {"plan": "test"}
Object to serialize
@Serializable
data class Test(
val plan: PlanAsString?
)
Serializer
typealias PlanAsString = @Serializable(with = PlanSerializer::class) String
class PlanSerializer : KSerializer<String?> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("StringSerializer", PrimitiveKind.STRING)
@OptIn(ExperimentalSerializationApi::class)
override fun deserialize(decoder: Decoder): String? {
kotlin.runCatching {
val plan = decoder.decodeSerializableValue(Test.serializer())
return plan.plan
}.onFailure {
return decoder.decodeString()
}
return decoder.decodeNull()
}
@OptIn(ExperimentalSerializationApi::class)
override fun serialize(encoder: Encoder, value: String?) {
return value?.let {
encoder.encodeString(value)
} ?: encoder.encodeNull()
}
}
And code where I am trying to deserializer string into Object
Json.decodeFromString<Test>("{\"plan\": \"test\"}") // This rows throws exception above
Json.decodeFromString<eu.abra.models.Test>("{\"plan\": {\"name\": \"basic\"}}") // This is deserialized correctly
If I understand correctly, your JSON can be
{"plan": "some name"}
or
{"plan": {"name": "some name"}}
And you want to serialise both formats into a Test
object with plan
being the string "some name"
.
Your approach doesn't work because after the decoder throws the exception when it fails to decode the expected object, it already consumes the first token of the JSON string ("
). By the time you catch the exception and call decodeString
, it will read the character after the "
, which is not what it expects.
You can do this using a JsonContentPolymorphicSerializer
- choose the correct serialiser based on whether the JSON element encountered is a JSON object or a JSON string.
object PlanSerializer : JsonContentPolymorphicSerializer<String>(String::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<String> =
if (element is JsonPrimitive && element.isString) {
String.serializer()
} else {
PlanAsStringSerializer
}
private object PlanAsStringSerializer : KSerializer<String> {
override val descriptor: SerialDescriptor = Plan.serializer().descriptor
override fun deserialize(decoder: Decoder): String =
decoder.decodeSerializableValue(Plan.serializer()).name
override fun serialize(encoder: Encoder, value: String) = error("Not supported")
}
}
@Serializable
data class Plan(
val name: String
)
If you want to fall back to null
when something goes wrong, you can decode a JsonElement
instead.
class PlanSerializer : KSerializer<String?> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("StringSerializer", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): String? {
return runCatching {
when(val plan = decoder.decodeSerializableValue(JsonElement.serializer())) {
is JsonPrimitive -> plan.takeIf { it.isString }?.content
is JsonObject -> Json.decodeFromJsonElement<Plan>(plan).name
else -> null
}
}.getOrNull()
}
@OptIn(ExperimentalSerializationApi::class)
override fun serialize(encoder: Encoder, value: String?) {
return value?.let {
encoder.encodeString(value)
} ?: encoder.encodeNull()
}
}