Jackson Scala JSON Deserialization to case classes

11,577

I think there's a few separate problems to address here, so I've listed three separate approaches.

TL;DR

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.

1. Custom Deserializer

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

2. Jackson Polymorphism

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"
    }
  } ]
}

3. Remove the polymorphism

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.

Share:
11,577
Mrugen Deshmukh
Author by

Mrugen Deshmukh

Graduate Student at San Jose State University, Software Engineer, quick learner

Updated on June 04, 2022

Comments

  • Mrugen Deshmukh
    Mrugen Deshmukh almost 2 years

    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.