Home > Net >  How can I deserialize a YAML where not all keys have the same subkeys?
How can I deserialize a YAML where not all keys have the same subkeys?

Time:04-01

Assume this YAML:

base:
  foo:
    a: 'a'
    b: 'b'
    c: 'c'
  bar:
    a: 'a'
    b: 'b'
    c: 'c'
  baz:
    a: 'a'
    t: 't'
    z: 'z'

All the documentation I have found about deserializing a YAML imply that every key holds the same data.

So, for example, ignoring baz, I would use these data classes:

data class Base(val base: Data)
data class Data(val foo: Values, val bar: Values)
data class Values(val a: String, val b: String, val c :String)

My file actually has over 50 keys at the level of foo,bar,baz, and many of them don't share the same keys (not even the same number), so how could I define my data classes to be able to serialize it?

I tried the following:

data class Base(val base: Data)
data class Data(val d: List<Map<String, Values>>)
data class Values(val v: List<Map<String, String>>)

But it complains because it's looking for an exact match for the keys.

CodePudding user response:

I assume you are using Jackson object mapper with support for yaml dataformat (com.fasterxml.jackson.dataformat:jackson-dataformat-yaml), and the Kotlin module (com.fasterxml.jackson.module:jackson-module-kotlin).

The data class Values in your example does not match all objects, because for instance it does not contain t and z which are contained in the base.baz value.

One possibility is to broaden the definition of Values to contain all possible values there can be, ignoring any values that are not there:

data class Values(
    val a: String?,
    val b: String?,
    val c: String?,
    val t: String?,
    val z: String?,
)

All values are nullable to allow them to be absent.

If you do not need all the values, you could also add a @JsonIgnoreProperties(ignoreUnknown = true) annotation, to tell Jackson that it is not a problem if there are values in the yaml that are not declared in your data class:

@JsonIgnoreProperties(ignoreUnknown = true)
data class Values(...

If this is not flexible enough, you can deserialize everything as a map:

val objectMapper = ObjectMapper(YAMLFactory()).registerKotlinModule()
val content: Map<String, *>  = objectMapper.readValue(theYaml)

Please note that in this case you gave Jackson no type information, and thus any non-trivial objects (i.e. not a String, Int, ...) in your yaml are deserialized as maps. Also, you don't have any type information yourself and you need to check every element if it is an instance of an expected type, and have to cast it manually. For instance, to obtain element base.foo.b you can then write:

val base = content["base"]
val foo = (base as Map<String, *>)["foo"]
val b = (foo as Map<String, *>)["b"]

If b is of the non-trivial type, let's say Something, it can converted to the right type using

val bWithType: Something = objectMapper.convertValue(b)

But note that this means that b is again serialized and then deserialized to get it with the right type. So the better solution would be the approach with the more flexible Values class above. The alternative should only be used if it is necessary.

  • Related