Reputation: 1411
Suppose you have a JSON which looks like this:
[{"foo": 1, "bar": 2}, {"foo": 3, "bar": {"baz": 4}}]
It seems natural to try to represent this using a Scala sum type:
sealed trait Item
case class IntItem(foo: Int, bar: Int) extends Item
case class Baz(baz: Int)
case class BazItem(foo: Int, bar: Baz) extends Item
My question is: is it possible to use Jackson's Scala module to serialize the JSON above into a List[Item]
?
My attempt:
val string = "[{\"foo\": 1, \"bar\": 2}, {\"foo\": 3, \"bar\": {\"baz\": 4}}]"
val mapper = new ObjectMapper() with ScalaObjectMapper
mapper.registerModule(DefaultScalaModule)
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
mapper.readValue[List[Item]](string)
The exception:
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of ...Item, problem: abstract types either need to be mapped to concrete types, have custom deserializer, or be instantiated with additional type information at [Source: [{"foo": 1, "bar": {"baz": 2}}, {"foo": 3, "bar": {"baz": 4}}]; line: 1, column: 2] (through reference chain: com.fasterxml.jackson.module.scala.deser.BuilderWrapper[0])
That makes it fairly clear what the problem is, but I'm not sure how best to fix it.
Upvotes: 2
Views: 2011
Reputation: 23788
As @Dima pointed out, I don't think there exists a generic solution that covers all the cases. Moreover I'm not sure it can exist at all because the difference might be hidden arbitrary deep and I suspect someone smart enough can create a halting problem from that. However many specific cases can be solved.
First of all, if you control both sides (serialization and deserialization), you should consider using JsonTypeIdResolver
annotation with some of TypeIdResolver
subclasses that will put name of the type in the JSON itself.
If you can't use JsonTypeIdResolver
, probably the only solution is to roll out your custom JsonDeserializer
as the error suggests. The example you provided in your question can be handled by something like this:
sealed trait Item
case class IntItem(foo: Int, bar: Int) extends Item
case class Baz(baz: Int)
case class BazItem(foo: Int, bar: Baz) extends Item
import com.fasterxml.jackson.core._
import com.fasterxml.jackson.databind._
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.util.TokenBuffer
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.node._
import com.fasterxml.jackson.databind.exc._
import java.io.IOException
class ItemDeserializer() extends StdDeserializer[Item](classOf[Item]) {
@throws[IOException]
@throws[JsonProcessingException]
def deserialize(jp: JsonParser, ctxt: DeserializationContext): Item = {
// 1) Buffer current state of the JsonParser
// 2) Use firstParser (from the buffer) to parser whole sub-tree into a generic JsonNode
// 3) Analyze tree to find out the real type to be parser
// 3) Using the buffer roll back history and create objectParser to parse the sub-tree as known type
val tb = new TokenBuffer(jp, ctxt)
tb.copyCurrentStructure(jp)
val firstParser = tb.asParser
firstParser.nextToken
val curNode = firstParser.getCodec.readTree[JsonNode](firstParser)
val objectParser = tb.asParser
objectParser.nextToken()
val bar = curNode.get("bar")
if (bar.isInstanceOf[IntNode]) {
objectParser.readValueAs[IntItem](classOf[IntItem])
}
else if (bar.isInstanceOf[ObjectNode]) {
objectParser.readValueAs[BazItem](classOf[BazItem])
}
else {
throw ctxt.reportBadDefinition[JsonMappingException](classOf[Item], "Unknown subtype of Item") // Jackson 2.9
//throw InvalidDefinitionException.from(jp, "Unknown subtype of Item", ctxt.constructType(classOf[Item])) // Jackson 2.8
}
}
}
and then you can use it as following
def test() = {
import com.fasterxml.jackson.module.scala._
import com.fasterxml.jackson.module.scala.experimental._
val mapper = new ObjectMapper() with ScalaObjectMapper
mapper.registerModule(DefaultScalaModule)
// add our custom ItemDeserializer
val module = new SimpleModule
module.addDeserializer(classOf[Item], new ItemDeserializer)
mapper.registerModule(module)
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
val string = "[{\"foo\": 1, \"bar\": 2}, {\"foo\": 3, \"bar\": {\"baz\": 4}}]"
val list = mapper.readValue[List[Item]](string)
println(list.mkString(", "))
}
which prints
IntItem(1,2), BazItem(3,Baz(4))
The main trick in the ItemDeserializer
is to use TokenBuffer
to parse JSON twice: first time to analyze the JSON-tree and find out as what type it should be parsed as, second time to actually parse the object of a known type.
Upvotes: 1