kotlinyamlkotlinx.serialization

Unwrap root value using kotlinx.seralization


Please help me with my problem. I have to deserialize this file using kotlinx.serialization and kaml

rootElement:
  id: 10
  name: Some name

to data class

@Serializable
data class SomeData(
    val id: Int,
    val name: String
)

but I don't know how to unwrap root element. In Jackson there's an annotation @JsonRootElement, but I cannot find anything similar in kotlinx.serialization. Is there a way to unwrap root element?

@Serializable
data class SomeData(
    val id: Int,
    val name: String
)

private val YAML = Yaml(configuration = YamlConfiguration(
    encodeDefaults = false,
    //no setting here to unwrap root element =(
))

fun main() {
    val fileData = File(ClassLoader.getSystemResource("yaml_file.yml").file).readText()
    val someData: SomeData = YAML.decodeFromString(fileData)
    println(someData)
}

I've tried Jackson but it is much heavier and it cannot deserialize inline classes when DeserializationFeature.UNWRAP_ROOT_VALUE is enabled

UPD

I also tried to parse file like Map<String, String> and take value by key, but kotlinx fails parsing yaml object as sring

@Serializable
data class SomeData(
    val id: Int,
    val name: String
)

private val YAML = Yaml(configuration = YamlConfiguration(
    encodeDefaults = false,
    //no setting here to unwrap root element =(
))

fun main() {
    val fileData = File(ClassLoader.getSystemResource("yaml_file.yml").file).readText()
    val rootName = "rootElement"

    val wrapper: Map<String, String> = YAML.decodeFromString(fileData)
    val data: SomeData? = wrapper[rootName]?.let { YAML.decodeFromString(it) }
    println(data)
}
Exception in thread "main" InvalidPropertyValueException at rootElement on line 2, column 3: Value for 'rootElement' is invalid: Expected a string, but got a map

Solution

  • The easiest solution is to declare a class for the root element as well. Then you can access the nested data structure.

    Something along the lines of the following should work:

    @Serializable
    data class SomeRoot(
        val rootElement: SomeData,
    )
    
    fun main() {
        val fileData: String = ...
        val someRoot: SomeRoot = YAML.decodeFromString(fileData)
        println(someRoot.rootElement)
    }
    

    In case you've got several different data types to support and don't want to create a wrapper class for each of them, you could decode the String into an YamlNode first and use that to retrieve the nested element which you decode afterwards. Something like this may work for you:

    fun main() {
        val fileData: String = ...
        val data: SomeData = YAML.decodeFromStringUnwrappingRoot(fileData)
        println(data)
    }
    
    private inline fun <reified T> Yaml.decodeFromStringUnwrappingRoot(string: String): T {
        val rootElement: YamlMap = YAML.parseToYamlNode(string).yamlMap
        val singleChildElement = rootElement.entries.values.single()
    
        return YAML.decodeFromYamlNode(serializersModule.serializer<T>(), singleChildElement)
    }