TovrikTheThird
TovrikTheThird

Reputation: 501

Jackson Deserializer default to null on failure

I have an issue where an upstream service is making requests and about 5% of the requests that are made are partially malformed. For example, instead of a nullable property coming to me as the object type I am expecting, I get a random string.

data class FooDTO(
  val bar: BarDTO?,
  val name: String
)

data class BarDTO(
  val size: Int
)

But the payload I get looks like

{
  "name": "The Name",
  "bar": "uh oh random string instead of object"
}

I don't want to fail the request when this happens because the part of the data that is correct is still useful for my purposes, so what I want to do is just default the deserialization failure to null. I also have a few different sub-objects in my FooDTO that do this so I want a generic way to solve it for those specific fields. I know I can write a custom deserializer like the following to solve it on a 1-off basis.

class BarDtoDeserializer @JvmOverloads constructor(vc: Class<*>? = null) : StdDeserializer<BarDTO?>(vc) {
    override fun deserialize(jp: JsonParser, ctxt: DeserializationContext): AnalysisDTO? {
        return try {
            val node = jp.codec.readTree<JsonNode>(jp)
            BarDTO(size = node.get("size").numberValue().toInt())
        } catch (ex: Throwable) {
            null
        }
    }
}

And then I can decorate my BarDTO object with a @JsonDeserialize(using=BarDtoDeserializer::class) to force it to use that deserializer. What I am hoping to do is have some way to do this in a generic way.

Upvotes: 2

Views: 1524

Answers (1)

aSemy
aSemy

Reputation: 7139

Thanks to this answer https://stackoverflow.com/a/45484422/4161471 I've found that a DeserializationProblemHandler can be used to return 'null' for invalid data.

In your instance, override the handleMissingInstantiator() function. If other payloads have other types of bad data, other overrides may be required.

Also, I thought that CoercionConfig might be the answer, but I couldn't get it to work or find decent documentation. That might be another path.


import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler
import com.fasterxml.jackson.databind.deser.ValueInstantiator
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue


data class FooDTO(
  val name: String,
  val bar: BarDTO?,
)

data class BarDTO(
  val size: Int
)

fun main() {
  val mapper = jacksonObjectMapper()

//  mapper
//    .coercionConfigFor(LogicalType.Textual)
//    .setCoercion(CoercionInputShape.String, CoercionAction.AsNull)

  mapper.addHandler(object : DeserializationProblemHandler() {
    override fun handleMissingInstantiator(
      ctxt: DeserializationContext?,
      instClass: Class<*>?,
      valueInsta: ValueInstantiator?,
      p: JsonParser?,
      msg: String?
    ): Any? {
      println("returning null for value, $msg")
      return null
    }
  })

  val a: FooDTO = mapper.readValue(
    """
      {
        "name": "I'm variant A",
        "bar": "uh oh random string instead of object"
      }
    """
  )
  val b: FooDTO = mapper.readValue(
    """
      {
        "name": "I'm variant B",
        "bar": {
          "size": 2     
        }
      }
    """
  )

  println(a)
  println(b)
  assert(a.bar == null)
  assert(a.bar?.size == 2)

}

Output:

returning null for value, no String-argument constructor/factory method to deserialize from String value ('uh oh random string instead of object')
FooDTO(name=I'm variant A, bar=null)
FooDTO(name=I'm variant B, bar=BarDTO(size=2))

Process finished with exit code 0

Upvotes: 3

Related Questions