Hi there. I need to parse a JSON where one field i...
# serialization
m
Hi there. I need to parse a JSON where one field is described as "can be either a String or an Array of Strings". Unfortunately, this is an external specification and cannot be changed. I've tried implementing a custom
KSerializer
for a Sealed Interface, where I would call
decoder.decodeString()
in the deserialize-Method and check if it starts with
[
. But that throws a
JsonDecodingException (Expected JsonPrimitive at ... but found ...)
when it encounters the "Array of Strings" option. What would be the correct approach for this scenario?
Is it safe to cast the
Decoder
to a
JsonDecoder
and then call
decodeJsonElement()
to check if it is a
JsonPrimitive
or a
JsonArray
?
g
Did you try with
JsonTransformingSerializer
? It allow you to operate on JsonElement directly, nice to play with those weird formats 🙂
m
Haven't looked at that class yet, but from the documentation it doesn't look like it achieves what I need to do. Transforming from one JsonElement to another still leaves me with the problem that I need to decode it into a Sealed Interface with two different data structures. Unless I'm understanding that class wrong?
v
You would transform the primitive into array and in the data object always have array, and optionally transform a one element array into primitive
g
I was thinking about always decoding with a List<String>, reading your message again, I guess you don't have only this field but other fields which are different? Can you share those classes?
m
Yes, I could transform it to an array with a single element, but since the specification distinguishes the two options, I would prefer to keep my data model in line with that. My sealed interface looks like this:
Copy code
@Serializable(with = EntryValueSerializer::class)
sealed interface EntryValue {
	@Serializable
	data class Reference(val reference: String) : EntryValue

	@Serializable
	data class Predefined(val mapping: Map<String, String>) : EntryValue
}
And I'm using that sealed interface like this:
Copy code
@Serializable
data class EntryRegistry(
	val entries: Map<String, EntryValue>,
	// More properties
)
The serializer I have right now and is working looks like this:
Copy code
override fun deserialize(decoder: Decoder): EntryValue {
		val jsonDecoder = decoder as? JsonDecoder ?: throw IllegalArgumentException("This class can only be deserialized from JSON")
		return when (val jsonElement = jsonDecoder.decodeJsonElement()) {
			is JsonPrimitive -> EntryValue.Reference(jsonElement.content)
			is JsonObject -> {
				EntryValue.Predefined(jsonElement.toMap().mapValues { it.value.jsonPrimitive.content })
			}
			else -> throw IllegalArgumentException("Excpected either JsonPrimitive or JsonArray but was $jsonElement")
		}
	}
Since I'm always serializing from/to JSON, the cast to
JsonDecoder
should be fine, right?
e
m
JsonContentPolymorphicSerializer looks like the more elegant solution to the JsonDecoder solution I'm using right now and which @ephemient also suggested. I'll try it out, thanks
Actually, that doesn't seem to work either, I get
Expected class kotlinx.serialization.json.JsonObject as the serialized body of EntryValue.Predefined, but had class kotlinx.serialization.json.JsonArray
Since the serializer for that sealed subclass expects to parse the string or string array into an object