Search code examples
kotlinjackson-databind

Mapping JSON field names as MyValue.name in List<MyValue> with Jackson


I have a JSON file:

{
  "alice": {
    age: 23
  },
  "bob": {
    age: 45
  }
}

I want to deserialise it to Collection<Person> with

data class Person(val name: String, val age: Int)

and serialise back to JSON again.

Can Jackson do this using annotations alone? Or do I need a custom JsonSerializer/JsonDeserializer?


Solution

  • Can Jackson do this using annotations alone?

    I believe it is not enough to use only annotations. However, there are a few other ways to solve the problem:

    1. One way to do it is to implement a custom deserialiser

    You may implement StdDeserializer from jackson-databind.

    This is an example which works with your code:

    const val JSON = """
    {
      "alice": {
        "age": 23
      },
      "bob": {
        "age": 45
      }
    }
    """
    
    fun main(args: Array<String>) {
        val mapper = ObjectMapper()
        val module = SimpleModule()
        module.addDeserializer(List::class.java, PersonDeserializer(Person::class.java))
        mapper.registerModule(module)
    
        val readValue = mapper.readValue(JSON, List::class.java)
        println(readValue)
    }
    
    data class Person(val name: String, val age: Int)
    
    class PersonDeserializer(vc: Class<*>? = null) : StdDeserializer<List<Person>>(vc) {
    
        override fun deserialize(p: JsonParser, ctx: DeserializationContext): List<Person> {
            val people = mutableListOf<Person>()
    
            val node: JsonNode = p.codec.readTree(p)
    
            if (node is ObjectNode) {
                for (field in node.fields()) {
                    val name = field.key
                    val age = field.value["age"].asInt()
    
                    people.add(Person(name, age))
                }
            }
    
            return people
        }
    }
    

    The code prints

    [Person(name=alice, age=23), Person(name=bob, age=45)]
    
    1. Another way to do so is to use HashMap and intermediate representation and then remap the object.

    See the example below:

    const val JSON = """
    {
      "alice": {
        "age": 23
      },
      "bob": {
        "age": 45
      }
    }
    """
    
    fun main(args: Array<String>) {
        val mapper = ObjectMapper()
    
        val readValue = mapper.readValue(JSON, object : TypeReference<HashMap<String, InterimPerson>>() {})
        println("Interim: $readValue")
    
        val finalValue = readValue.map { Person(it.key, it.value.age) }.toList()
        println(finalValue)
    }
    
    @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
    data class InterimPerson(@JsonProperty("age") var age: Int)
    
    data class Person(val name: String, val age: Int)
    

    The code yields:

    Interim: {bob=InterimPerson(age=45), alice=InterimPerson(age=23)}
    [Person(name=bob, age=45), Person(name=alice, age=23)]