Skip to content

Jackson doesn't invoke default constructor of a case class on deserialization #330

@batytskyy

Description

@batytskyy

Jackson version 2.8.9, 2.8.6:
Say, the following case class is defined:
case class Result(data: Seq[Int] = Seq.empty) { def this() = this(Seq(1)) }

Then, if we write the following test:

import com.fasterxml.jackson.annotation.JsonInclude.Include
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper
import org.scalatest.WordSpec

class DeserilizationTest extends WordSpec {

  "Object mapper" when {
    val Mapper = new ObjectMapper() with ScalaObjectMapper
    val module = new SimpleModule
    Mapper
      .registerModule(module)
      .registerModule(DefaultScalaModule)
      .setSerializationInclusion(Include.NON_EMPTY)

    "deserializing case class with a default constructor" in {
      val result = Result()
      val serialized = Mapper.writeValueAsString(result)
      val deserialized = Mapper.readValue(serialized.getBytes, classOf[Result])
      assert(deserialized.data === Seq(1))
    }
  }
}

case class Result(data: Seq[Int] = Seq.empty) {
  def this() = this(Seq(1))
}

It will fail in a few runs because deserialized.data would equal to null.

The root cause is the:
com.fasterxml.jackson.module.scala.introspect.BeanIntrospector#def findConstructorParam(c: Class[_], name: String): Option[ConstructorParameter]

This method resolves
val primaryConstructor = c.getConstructors.headOption
If primaryConstructor is resolved to a default constructor with no arguments, then findConstructorParam will eventually return None. If None is returned, BeanDeserializerBase._vanillaProcessing will eventually be set to true, this will make Jackson call the default constructor of Result.

But, if the primaryConstructor is resolved to the constructor with the argument, then findConstructorParam will return Some(), which eventually leads Jackson to ignore the default constructor.

The primaryConstructor resolution should be changed to something like:
c.getConstructors.find(findADefaultConstructor).orElse(c.getConstructors.headOption)

Note:
The issue of not calling the default constructor can be eliminated by annotating it with @JsonCreator as follows:
case class Result(data: Seq[Int] = Seq.empty) { @JsonCreator def this() = this(Seq(1)) }

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions