I have a JSON which has following form:
{
"inventory": [
{
"productType": "someProduct1",
"details": {
"productId": "Some_id",
"description": "some description"
}
},
{
"productType": "someProduct2",
"details": {
"productId": "Some_id",
"description":{"someKey":"somevalue"}
}
}
]
}
The case classes that I want the above json to deserialize look like following:
case class Inventory(products:List[Product])
case class Product(productType:String,details:ProductDetails)
abstract class ProductDetails
case class ProductDetailsSimple(productId:String,description:String) extends ProductDetails
case class ProductDetailsComplex(productId:String,description:Map[String,String]) extends ProductDetails
I am using jackson-scala module to deserialize the above JSON string as follows:
val mapper = new ObjectMapper() with ScalaObjectMapper
mapper.registerModule(DefaultScalaModule)
mapper.readValue(jsonBody, classOf[Inventory])
The error I get is as follows: "Unexpected token (END_OBJECT), expected FIELD_NAME: missing property '@details' that is to contain type id (for class ProductDetails)\n at [Source: java.io.StringReader@12dfbabd; line: 9, column: 5]"
I have been through jackson documentation on Polymorphic deserialization and have tried combinations as mentioned but with no luck. I would like to understand what I am doing wrong here, which needs correction with respect to deserialization using jackson module.
I think there's a few separate problems to address here, so I've listed three separate approaches.
Either use Jackson polymorphism correctly or, in your case, go to a simpler approach and remove the need for the polymorphism. See my code on github.
Your formatted JSON is:
{ inventory:
[ { productType: 'someProduct1',
details:
{ productId: 'Some_id',
description: 'some description' } },
{ productType: 'someProduct2',
details:
{ productId: 'Some_id',
description: { someKey: 'somevalue' }
}
}
]
}
The field productType
is misplaced, in my opinion, but if this format is a strict requirement then you could write your own deserializer that looks at the productType
field and instantiates a different concrete class.
I don't think this would be the best solution so I didn't write example code, but I like the Joda date-time package as a reference for custom serialize/deserialize
You've separated Product
from ProductDetails
with a type field:
case class Product(productType:String,details:ProductDetails)
abstract class ProductDetails
I think you've confused how Jackson's polymorphic data type handling works and complicated your class design as a result.
Perhaps your business rules require that a product has a "type", in which case I'd name it "kind" or some other non-code label, and put it into what you've called ProductDetails
.
But if "type" was included in an attempt to get type polymorphism working, then it isn't the right way.
I've included the below as a working example of Jackson polymorphism in Scala:
/**
* The types here are close to the original question types but use
* Jackson annotations to mark the polymorphic JSON treatment.
*/
import scala.Array
import com.fasterxml.jackson.annotation.JsonSubTypes.Type
import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo}
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
@JsonSubTypes(Array(
new Type(value = classOf[ProductDetailsSimple], name = "simple"),
new Type(value = classOf[ProductDetailsComplex], name = "complex")
))
abstract class Product
case class ProductDetailsSimple(productId: String, description: String) extends Product
case class ProductDetailsComplex(productId: String, description: Map[String, String]) extends Product
case class PolymorphicInventory(products: List[Product])
Note that I removed the Product
vs ProductDetails
distinction, so an Inventory
now just as a list of Product
. I left the names ProductDetailsSimple
and ProductDetailsComplex
though I think they should be renamed.
Example usage:
val inv = PolymorphicInventory(
List(
ProductDetailsSimple(productId="Some_id", description="some description"),
ProductDetailsComplex(productId="Some_id", description=Map("someKey" -> "somevalue"))
)
)
val s = jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(inv)
println("Polymorphic Inventory as JSON: "+s)
Output:
Polymorphic Inventory as JSON: {
"products" : [ {
"type" : "simple",
"productId" : "Some_id",
"description" : "some description"
}, {
"type" : "complex",
"productId" : "Some_id",
"description" : {
"someKey" : "somevalue"
}
} ]
}
I suggest that polymorphism in this case isn't needed at all, and that the error is in trying to make "description" either a single string or a key/value map when they are really fields with distinct intentions.
Perhaps there is a data legacy issue involved (in which case see the custom deser suggestion), but if the data is in your control, I vote for "go simpler":
case class Product(productId: String,
description: String="",
attributes: Map[String, String]=Map.empty)
case class PlainInventory(products: List[Product])
I's more "scala-rific" to use Option
to indicate the absence of a value, so:
case class Product(productId: String,
description: Option[String]=None,
attributes: Option[Map[String, String]]=None)
Example usage:
val inv = PlainInventory(
List(
Product(productId="Some_id", description=Some("some description")),
Product(productId="Some_id", attributes=Some(Map("someKey" -> "somevalue")))
)
)
val s = jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(inv)
println("Plain Inventory as JSON: "+s)
Output:
Plain Inventory as JSON: {
"products" : [ {
"productId" : "Some_id",
"description" : "some description"
}, {
"productId" : "Some_id",
"attributes" : {
"someKey" : "somevalue"
}
} ]
}
Working minimal code on github.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With