3

How to parse a JSON conditionally before deserialisation to the following case class:

case class UserInfo(id: String, startDate: String, endDate: String)

I have an implicit reads

object UserInfo {
    implicit val reads: Reads[UserInfo] = (
        (__ \ "id").read[String] and
        (__ \ "startDate").read[String] and
        (__ \ "endDate").read[String]
    )(UserInfo.apply _)
   }

I can parse the following json using above implicit reads

 val jsonString = """
{
       "users":[
          {
             "id":"123",
             "startDate":"2019-06-07",
             "endDate":"2019-06-17"
          },
          {
             "id":"333",
             "startDate":"2019-06-07",
             "endDate":"2019-06-27"
          }
       ]
    }"""

val userInfoList = (Json.parse(jsonString) \ "users").as[List[UserInfo]]

but sometimes the web service returns a JSON with no startDate and endDate, for example:

{
   "users":[
      {
         "id":"123",
         "startDate":"2019-06-07",
         "endDate":"2019-06-17"
      },
      {
         "id":"333",
         "startDate":"2019-06-07"
      },
      {
         "id":"444"
      }
   ]
}

How to conditionally parse json to ignore objects that don't have startDate or endDate without making those fields optional in UserInfo model?

Mario Galic
  • 47,285
  • 6
  • 56
  • 98
vkt
  • 1,401
  • 2
  • 20
  • 46
  • Either you make those fields `Option`al (BTW date as `String` in case class seams weird), or you have to ignore `JsError` on JSON validation (not responsibility of the `Reads` in such case) – cchantep Jun 14 '19 at 15:09
  • >BTW date as String in case class seams weird. I just used for example purpose. – vkt Jun 14 '19 at 15:16

2 Answers2

3

To avoid changing the model to optional fields we could define coast-to-coast transformer which filters out users with missing dates like so

    val filterUsersWithMissingDatesTransformer = (__ \ 'users).json.update(__.read[JsArray].map {
      case JsArray(values) => JsArray(values.filter { user =>
        val startDateOpt = (user \ "startDate").asOpt[String]
        val endDateOpt = (user \ "endDate").asOpt[String]
        startDateOpt.isDefined && endDateOpt.isDefined
      })
    })

which given

    val jsonString =
      """
        |{
        |   "users":[
        |      {
        |         "id":"123",
        |         "startDate":"2019-06-07",
        |         "endDate":"2019-06-17"
        |      },
        |      {
        |         "id":"333",
        |         "startDate":"2019-06-07"
        |      },
        |      {
        |         "id":"444"
        |      }
        |   ]
        |}
      """.stripMargin

    val filteredUsers = Json.parse(jsonString).transform(filterUsersWithMissingDatesTransformer)
    println(filteredUsers.get)

outputs

{
  "users": [
    {
      "id": "123",
      "startDate": "2019-06-07",
      "endDate": "2019-06-17"
    }
  ]
}

meaning we can deserialise to the existing model without making startDate and endDate optional.

case class UserInfo(id: String, startDate: String, endDate: String)
Mario Galic
  • 47,285
  • 6
  • 56
  • 98
1

You can use Option for this:

case class UserInfo(id: String, startDate: Option[String], endDate: Option[String])

object UserInfo {
    implicit val reads: Reads[UserInfo] = (
        (__ \ "id").read[String] and
        (__ \ "startDate").readNullable[String] and
        (__ \ "endDate").readNullable[String]
    )(UserInfo.apply _)
   } 

This would work when startDate and endDate are not provided.

Valy Dia
  • 2,781
  • 2
  • 12
  • 32
  • 1
    I was wondering if there is any way without making those optional.ie.e silently ignore those objects that don't have the `startDate/endDate.` – vkt Jun 14 '19 at 15:15
  • How would you expect it to be optional in the `Reads` if not `Option` ? – cchantep Jun 14 '19 at 15:37
  • Mario answer worked for me https://stackoverflow.com/a/56601051/4187091 – vkt Jun 14 '19 at 17:26