zasaz
zasaz

Reputation: 2524

Kotlin reading json unknown type Spring

I'm calling different APIs, that use the same key name in the JSON file. Depending on the response, there's one field that may be different types.

To be clear:

My code looks like this when using the second API:

data class Result(
    @SerializedName("results") var persons:ArrayList<Person> =ArrayList()
)

The question is if there's any way to use the same class, without taking care if it's a JSON array or a JSON object.

Upvotes: 0

Views: 414

Answers (2)

Maciej Walkowiak
Maciej Walkowiak

Reputation: 12932

You can use ObjectMapper.typeFactory.constructParametricType to handle generic types:

data class Result<T>(
  var x:T
)

val om = ObjectMapper()
om.registerModule(KotlinModule())
val parsedList = om.readValue<Result<List<String>>>(
  """{"x":["x1", "x2"]}""", 
  om.typeFactory.constructParametricType(Result::class.java, List::class.java)
)
println(parsedList)
val parsedMap = om.readValue<Result<Map<String, String>>>(
  """{"x":{"k1": "v1", "k2": "v2"}}""", 
  om.typeFactory.constructParametricType(Result::class.java, Map::class.java)
)
println(parsedMap)

Gives output:

Result(x=[x1, x2])
Result(x={k1=v1, k2=v2})

Upvotes: 1

BDD
BDD

Reputation: 41

I believe you can define results as an instance of com.fasterxml.jackson.databind.JsonNode.

data class Result(
    val results: JsonNode
)    

Then you can process results based on it's type—whether it is an ArrayNode or an ObjectNode (as both extend JsonNode):

fun processResults(results: JsonNode) = when{
  results.isArray -> processArrayNode(results)
  else -> processObjectNode(results)
}

private fun processArrayNode(list: JsonNode): *return whatever you need*{
  val elements = list
             .elements()
             .asSequence()
             .toList()

  val mappedElements = elements.map{
    processObjectNode(it)
  }

// do whatever you need with the array
}

private fun processObjectNode(person: JsonNode): *return whatever you need*{
  //** this will transform the json node into a linkedHashMap where the keys are the json keys and the values are the values (here interpreted as jsonNodes) **/
  val fieldsMap = person
                .fields()
                .asSequence()
                .associateBy( {it.key}, {it.value} )
  
  // process whatever you need
}

This is one way to use the same DTO for both API calls. In my opinion, it is not worth the extra work. I would create two DTOs containing the results field, where in one it is an instance of Person, and in the other it is an instance of List<Person>.

Edit: One little upgrade to the above snippet would be to add extension methods to JsonNode:

fun JsonNode.elementsToList(): List<JsonNode> = this
    .elements()
    .asSequence()
    .toList()

fun JsonNode.fieldsToMap(): Map<String, JsonNode> = this
    .fields()
    .asSequence()
    .associateBy({it.key}, {it.value})

Upvotes: 4

Related Questions