Dynamic type casting for JsValue field in Scala Play

Since I'm writing a function to request data from another API in my Scala code, the response Json has the format like this:

"data": {
      "attributeName": "some String",
      "attributeValue": false,
      "attributeSource": "Manual",
      "attributeValueLabel": null
  },
 "data": {
      "attributeName": "some String",
      "attributeValue": "daily",
      "attributeSource": "Manual",
      "attributeValueLabel": "Almost Daily"
  }

Note that sometimes the type of attributeValue is String value, some other time it's a Boolean value.

So I'm trying to write my own Reads and Writes to read the type dynamically.

case class Data(attributeName: Option[String], attributeValue: Option[String], attributeSource: Option[String], attributeValueLabel: Option[String])

object Data{

  implicit val readsData: Reads[Data] = {
    new Reads[Data] {
      def reads(json: JsValue) = {
        val attrValue = (json \ "attributeValue").as[] // How to cast to Boolean some time, but some other time is a String here
        ......
      }
    }
  }

So as you can see in my comment, I'm stuck at the part to cast the (json \ "attributeValue") to String/Boolean, base on the return type of the API. How can I do this?

2 answers

  • answered 2017-11-12 20:36 Radu

    You can try to parse it as String first and then as Boolean:

    val strO = (json \ "attributeValue").asOpt[String]
    val value: Option[String] = strO match {
      case str@Some(_) => str
      case None        => (json \ "attributeValue").asOpt[Boolean].map(_.toString)
    }
    

  • answered 2017-11-12 21:17 Tyler

    You can use the .orElse function when you are trying to read an attribute in different ways:

    import play.api.libs.json.{JsPath, Json, Reads}
    import play.api.libs.functional.syntax._
    
    
    val json1 =
     """
       |{
       |  "attributeName": "some String",
       |  "attributeValue": false
       |}
     """.stripMargin
    
    val json2 =
      """
        |{
        |  "attributeName": "some String",
        |  "attributeValue": "daily"
        |}
      """.stripMargin
    
    // I modified you case class to make the example short
    case class Data(attributeName: String, attributeValue: String)
    object Data {
    
      // No need to define a reads function, just assign the value
      implicit val readsData: Reads[Data] = (
        (JsPath \ "attributeName").read[String] and
    
        // Try to read String, then fallback to Boolean (which maps into String)
        (JsPath \ "attributeValue").read[String].orElse((JsPath \ "attributeValue").read[Boolean].map(_.toString))
      )(Data.apply _)
    }
    
    println(Json.parse(json1).as[Data])
    println(Json.parse(json2).as[Data])
    

    Output:

    Data(some String,false)
    Data(some String,daily)