How to parse unknown/changing JSON data type with kotlinx.serialization.json library?

I have a JSON string to parse in which an element’s type can change from Object to an Array or can be Object.

  1. How do we deal with this changing data type when creating a @Serializable data class in kotlin? In my example JSON string below, the “work” key can change between object or an array.
  2. In a dynamic language like python, I would decode the JSON string and check it’s data type before using it. Is something like this possible in Kotlin with kotlinx.serialization.json library?

JSON String:

{
    "name": null,
    "fullname": "User name",
    "age": 38,
    "weight": 100.55,
    "hobbies": ["reading", "internet", "sleeping", "eating"],
    "work": {
        "company": "Some AI",
        "joining_date": 2021
    }
}

The @Serializable class in Kotlin:

@Serializable
data class Work(
    var company: String? = null,
    var joining_date: Int? = null
)

@Serializable
data class Data(
    var name: String? = null,
    var fullname: String? = null,
    var age: Int? = null,
    var weight: Double? = null,
    var hobbies: List<String>? = null,
    var work: Work? = null
)

val json = Json.decodeFromString<Data>(json_str)

For dynamic schemas we can always deserialize to JsonObject or create custom serializers. But it’s best to avoid such flexible schemas in the first place - no matter the language we use.

2 Likes

Based on your description I suspect that you probably want to implement a custom polymorphic deserializer, but could you first provide a more complete example?

@madmax1028

Example JSON with object:

{
    "name": null,
    "fullname": "User name",
    "age": 38,
    "weight": 100.55,
    "hobbies": ["reading", "internet", "sleeping", "eating"],
    "work": {
        "company": "Some AI",
        "joining_date": 2021
    }
}

Example JSON with Array:

{
    "name": null,
    "fullname": "User name",
    "age": 38,
    "weight": 100.55,
    "hobbies": ["reading", "internet", "sleeping", "eating"],
    "work": ["test"]
}

From the above it is not even clear what’s the expected deserialized data.

This is how I would tackle the problem:

@Serializable(with = WorkSerializer::class)
sealed interface Work {
	@Serializable(with = WorkListSerializer::class)
	class WorkList(private val works: List<String>) : Work, List<String> by works {
		override fun hashCode() = works.hashCode()
		override fun equals(other: Any?) = works == other
		override fun toString() = works.toString()
	}
	
	@Serializable
	data class DetailedWork(
		val company: String,
		@SerialName("joining_date")
		val joiningYear: Int,
	) : Work
}

object WorkListSerializer : KSerializer<Work.WorkList> {
	@OptIn(ExperimentalSerializationApi::class)
	override val descriptor = listSerialDescriptor<String>()
	
	override fun deserialize(decoder: Decoder): Work.WorkList {
		return Work.WorkList(works = decoder.decodeSerializableValue(ListSerializer(String.serializer())))
	}
	
	override fun serialize(encoder: Encoder, value: Work.WorkList) {
		encoder.encodeSerializableValue(ListSerializer(String.serializer()), value)
	}
}

object WorkSerializer : JsonContentPolymorphicSerializer<Work>(Work::class) {
	override fun selectDeserializer(element: JsonElement) = when (element) {
		is JsonArray -> Work.WorkList.serializer()
		is JsonObject -> Work.DetailedWork.serializer()
		else -> throw SerializationException("Cannot determine the type to deserialize: $element")
	}
}

@Serializable
data class Data(
	val name: String?,
	@SerialName("fullname")
	val fullName: String,
	val age: Int,
	val weight: Double,
	val hobbies: List<String>,
	val work: Work,
)

@OptIn(ExperimentalSerializationApi::class)
fun main() {
	val json = Json {
		prettyPrint = true
		prettyPrintIndent = "\t"
	}
	
	run {
		val data = json.decodeFromString<Data>("""
			{
				"name": null,
				"fullname": "User name",
				"age": 38,
				"weight": 100.55,
				"hobbies": ["reading", "internet", "sleeping", "eating"],
				"work": {
					"company": "Some AI",
					"joining_date": 2021
				}
			}
		""".trimIndent()).also(::println)
		json.encodeToString(data).let(::println)
	}
	
	run {
		val data = json.decodeFromString<Data>("""
			{
				"name": null,
				"fullname": "User name",
				"age": 38,
				"weight": 100.55,
				"hobbies": ["reading", "internet", "sleeping", "eating"],
				"work": ["test"]
			}
		""".trimIndent()).also(::println)
		json.encodeToString(data).let(::println)
	}
}
2 Likes