Search code examples
jsonscalaplayframeworkplay-json

Conditional filtering of JSON before deserialisation to case class model


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?


Solution

  • 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)