ElGatoGabe
ElGatoGabe

Reputation: 143

Convert JSON to case class with a nested objects using Scala/Play

Say the JSON response I'm working with is formatted as follows:

[
    {
       "make": "Tesla",
       "model": "Model S",
       "year": 2017,
       "color": "red",
       "owner": "Bob",
       "max_speed": 200,
       "wheel_size": 30,
       "is_convertible": true,
       "license": "ABC123",
       "cost": 50000,
       "down_payment": 2500,
       "other_property_1": 1,
       "other_property_2": 2,
       "other_property_3": 3,
       "other_property_4": 4,
       "other_property_5": 5,
       "other_property_6": 6,
       "other_property_7": 7,
       "other_property_8": 8,
       "other_property_9": 9,
       "other_property_10": 10,
       "other_property_11": 11
    }
]

The JSON here is an array of car objects (just 1 for simplicity), and I am trying to convert this into a model using a JSON Reads converter. Let's say I have a Car case class to represent each object, and that class has has a nested FinancialInfo case class to split up the amount of attributes logically, so to avoid Scala's 22 parameter limit.

import play.api.libs.functional.syntax._
import play.api.libs.json._

case class Car(
    make: String,
    model: String,
    year: Int,
    color: String,
    owner: String,
    maxSpeed: Int,
    wheelSize: Int,
    isConvertible: Boolean,
    license: String,
    financialInfo: FinancialInfo, // nested case class to avoid 22 param limit
    otherProperty1: Int,
    otherProperty2: Int,
    otherProperty3: Int,
    otherProperty4: Int,
    otherProperty5: Int,
    otherProperty6: Int,
    otherProperty7: Int,
    otherProperty8: Int,
    otherProperty9: Int,
    otherProperty10: Int,
    otherProperty11: Int
)

object Car {
    implicit val reads: Reads[Car] = (
        (__ \ "make").read[String] and
        (__ \ "model").read[String] and
        (__ \ "year").read[Int] and
        (__ \ "color").read[String] and
        (__ \ "owner").read[String] and
        (__ \ "max_speed").read[Int] and
        (__ \ "wheel_size").read[Int] and
        (__ \ "is_convertible").read[Boolean] and
        (__ \ "license").read[String] and
        (__ \ "financialInfo").read[FinancialInfo] and
        (__ \ "other_property_1").read[Int] and
        (__ \ "other_property_2").read[Int] and
        (__ \ "other_property_3").read[Int] and
        (__ \ "other_property_4").read[Int] and
        (__ \ "other_property_5").read[Int] and
        (__ \ "other_property_6").read[Int] and
        (__ \ "other_property_7").read[Int] and
        (__ \ "other_property_8").read[Int] and
        (__ \ "other_property_9").read[Int] and
        (__ \ "other_property_10").read[Int] and
        (__ \ "other_property_11").read[Int]
    )(Car.apply _)
}

case class FinancialInfo(
   cost: BigDecimal,
   downPayment: BigDecimal
)

object FinancialInfo {
    implicit val reads: Reads[FinancialInfo] = (
        (__ \ "cost").read[BigDecimal] and
        (__ \ "down_payment").read[BigDecimal]
    )(FinancialInfo.apply _)
}

However, I'm guessing since there is no property in the JSON called financialInfo, it is not parsing it correctly. In my real application, I'm getting this error when I use response.json.validate[List[Car]]:

JsError(List(((0)/financialInfo,List(JsonValidationError(List(error.path.missing),WrappedArray()))))) 

To summarize, in the example, cost and down_payment are not contained in a nested object, even though for the Car case class I had to include a nested model called financialInfo. What is the best way to work around this error and make sure the values for cost and down_payment can be parsed? Any help or insight would be greatly appreciated!

Upvotes: 4

Views: 4527

Answers (1)

Antot
Antot

Reputation: 3964

Reads can be combined and included into each other.

So, having:

implicit val fiReads: Reads[FinancialInfo] = (
  (JsPath \ "cost").read[BigDecimal] and
  (JsPath \ "down_payment").read[BigDecimal]
  )(FinancialInfo.apply _)

We can include it into the parent Reads:

implicit val carReads: Reads[Car] = (
  (JsPath \ "make").read[String] and
  (JsPath \ "model").read[String] and
  fiReads  // <--- HERE!
)(Car.apply _)

Now, with the following JSON:

private val json =
  """
    |[
    |    {
    |       "make": "Tesla",
    |       "model": "Model S",
    |       "cost": 50000,
    |       "down_payment": 2500
    |    },
    |    {
    |       "make": "Tesla",
    |       "model": "Model D",
    |       "cost": 30000,
    |       "down_payment": 1500
    |    }
    |]
  """.stripMargin

val parsedJsValue = Json.parse(json)
val parsed = Json.fromJson[List[Car]](parsedJsValue)

println(parsed)

It is parsed properly:

JsSuccess(List(Car(Tesla,Model S,FinancialInfo(50000,2500)), Car(Tesla,Model D,FinancialInfo(30000,1500))),)

p.s. The Reads in the original question do no need to be wrapped into different objects. Related implicit values would be better inside same scope, closer to where they are actually used.

Upvotes: 7

Related Questions