3
votes

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?

2
Either you make those fields Optional (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
>BTW date as String in case class seams weird. I just used for example purpose.vkt

2 Answers

3
votes

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)
1
votes

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.