Search code examples
jsonkotlinjackson-databind

Implicitly convert old version of a data model


I have a data structure and I am serializing this data structure to a JSON-File using Jackson Databind. As time progresses, the data model changes, but my application still needs to be able to read an old version of the JSON. My intention is that when an old version of the JSON is encountered, it is implicitly converted to the new version of the data structure in memory and when serialized for the next time, it is stored as the new format version.

For the case of newly added properties, this is simple: I simply specify a default value in Kotlin and Jackson uses that default value if the property is missing in the JSON. However, this case is more complicated: Previously, I had the following data structure:

data class Options(
    var applyClahePerColorChannel: Boolean = false
)

Now, I want to make this more general and change the data structure to the following:

data class Options(
    var multichannelMode: MultichannelMode = MultichannelMode.ApplyToLuminance
)

enum class MultichannelMode {
    ApplyToLuminance, ApplyToAllColorsSeparately
}

Now, when reading an old version of the JSON, applyClahePerColorChannel == false should implicitly be translated to multichannelMode == ApplyToLuminance and applyClahePerColorChannel == true to multichannelMode == ApplyToAllColorsSeparately.

How can I achieve that in Jackson in a concise way?


Solution

  • Here's a solution I found, but I'm still open to better suggestions.

    A simple renamed property can be handled with @JsonAlias like so:

    If this data model...

    data class Options(
        var applyClahePerColorChannel: Boolean = false
    )
    

    ...changes to...

    data class Options(
        var applyPerColorChannel: Boolean = false
    )
    

    ...the old name can be added as an alias like so:

    data class Options(
        @get:JsonAlias("applyClahePerColorChannel")
        var applyPerColorChannel: Boolean = false
    )
    

    Jackson will then treat both the same way, but upon serialization, Jackson will use the new name.

    However, in my case, I also changed the type of the variable, requiring a custom converter like so:

    data class Options(
        @get:JsonAlias("applyClahePerColorChannel")
        @get:JsonDeserialize(converter = BooleanToMultichannelModeConverter::class)
        var multichannelMode: MultichannelMode = MultichannelMode.ApplyToLuminance
    )
    
    class BooleanToMultichannelModeConverter : Converter<String, MultichannelMode> {
        override fun convert(value: String): MultichannelMode {
            return when (value) {
                in listOf("true", "True", "TRUE") -> ApplyToAllColorsSeparately
                in listOf("false", "False", "FALSE") -> ApplyToLuminance
                else -> MultichannelMode.valueOf(value)
            }
        }
    
        @OptIn(ExperimentalStdlibApi::class)
        override fun getInputType(typeFactory: TypeFactory): JavaType = typeFactory
            .constructType(String::class.starProjectedType.javaType)
    
        @OptIn(ExperimentalStdlibApi::class)
        override fun getOutputType(typeFactory: TypeFactory): JavaType = typeFactory
            .constructType(MultichannelMode::class.starProjectedType.javaType)
    }
    

    The converter basically tells Jackson to not do any parsing on its own and instead simply hand the unparsed string to the converter class, which will first attempt to parse the string as a boolean and if that fails as an enum value.