Search code examples
jsonscalaplayframeworkplay-json

Parsing JSON with Scala play framework - handling variable array elements?


I'm trying to parse a json string of search results of the following form:

"""
{
  "metadata": [
    "blah",
    "unimportant"
  ],
  "result": [
    {
      "type": "one",
      "title": "this",
      "weird": "attribute unique to this type",
      "other": 7
    },
    {
      "type": "two",
      "title": "that",
      "another_weird": "a different attribute unique to this second type",
      "another": "you get the idea"
    },
    {
      "type": "one",
      "title": "back to this type, which has the same fields as the other element of this type",
      "weird": "yep",
      "other": 8
    }
    ...
  ]
}
"""

There is a known, fixed number of result element types (given by the type field), each of which correspond to a unique schema. For a given search request, there can be any number of each type returned in any order (up to some fixed total).

Writing out the case classes and Reads implicits for each type is easy enough, but my question is around the best way to handle the variability in the result sequence... pattern matching seems the obvious choice, but I'm just not sure where or how with the play framework. Thanks in advance!

EDIT: Per the suggestion in the comments, I gave it a go by attempting to read the result sequence as subtypes of a common base trait, but that didn't quite work. The following compiles, but doesn't parse the example json correctly. Any additional suggestions are welcome.

sealed trait Parent {def title: String}
case class One(`type`: String, title: String, weird: String, other: Int) extends Parent
case class Two(`type`: String, title: String, another_weird: String, another: String) extends Parent

case class SearchResponse(result: Seq[Parent], metadata: Seq[String])

implicit val oneReader = Json.reads[One]
implicit val twoReader = Json.reads[Two]
implicit val parentReader = Json.reads[Parent]

implicit val searchReader = (
  (__ \ "result").read[Seq[Parent]] and (__ \ "metadata").read[Seq[String]]
)(SearchResponse.apply _)

val searchResult = Json.fromJson[SearchResponse](json)
print(searchResult)

Solution

  • Define implicit JsonConfiguration

    import play.api.libs.json._
    
    sealed trait Parent {
      def title: String
    }
    case class One(title: String, weird: String, other: Int) extends Parent
    case class Two(title: String, another_weird: String, another: String) extends Parent
    
    case class SearchResponse(result: Seq[Parent], metadata: Seq[String])
    
    implicit val cfg = JsonConfiguration(
      discriminator = "type",
      typeNaming = JsonNaming(_.toLowerCase)
    )
    
    implicit val oneReader    = Json.reads[One]
    implicit val twoReader    = Json.reads[Two]
    implicit val parentReader = Json.reads[Parent]
    implicit val searchReader = Json.reads[SearchResponse]
    
    val searchResult = Json.fromJson[SearchResponse](json)
    println(searchResult)
    // JsSuccess(SearchResponse(List(One(this,attribute unique to this type,7), Two(that,a different attribute unique to this second type,you get the idea), One(back to this type, which has the same fields as the other element of this type,yep,8)),List(blah, unimportant)),)
    

    https://www.playframework.com/documentation/2.8.x/ScalaJsonAutomated#Custom-Naming-Strategies