Search code examples
scalaplay-json

How to extract extra (key,value) from Json using play-json in scala?


I have a following scenario:

case class Person(id: Int, name: String)
val json = Json.obj("id" -> 1, "name" -> "John", "address"-> "Paris", "contact" -> "1234")

Here, I want to extract extra (key,value) from json i.e {"address"-> "Paris", "contact" -> "1234"} that do not belong to Person.

I have developed following approach so far:

case class Person(id: Int, name: String)
  val personReads = Json.reads[Person]
  val personWrites = Json.writes[Person]
  val json = Json.obj("id" -> 1, "name" -> "John", "address"-> "Paris", "contact" -> "1234")

  val person: Person = personReads.reads(json).get

  // This person json does not have extra fields 
  val personJson: JsObject = personWrites.writes(person).asInstanceOf[JsObject]

  val extraKeys = json.keys.diff(personJson.keys)

  val extraJson = extraKeys.foldLeft(Json.obj()){(result,key) =>
                            result.+(key -> json.\(key).get)}

  // {"address":"Paris","contact":"1234"}

This works but here I have to do a lot of json to case class conversions. What would be the best way to extract extra (key,value) in this scenario ?


Solution

  • If you want to make it general for any case class and not doing anything fancy with custom Reads, you can use reflection or shapeless to extract case class names and then remove those from an object you're trying to parse.

    E.g. using reflection, this only creates a case class instance once and does not require Writes at all:

    import play.api.libs.json._
    import scala.reflect.runtime.universe._
    
    def withExtra[A: Reads: TypeTag]: Reads[(A, JsObject)] = {
      val ccFieldNames = typeOf[A].members.collect {
        case m: MethodSymbol if m.isCaseAccessor => m.name.toString
        }.toVector
    
      for {
        jsObj <- implicitly[Reads[JsObject]]
        a <- implicitly[Reads[A]]
        filteredObj = ccFieldNames.foldLeft(jsObj)(_ - _)
      } yield (a, filteredObj)
    }
    

    And use it e.g. like so:

    case class Person(id: Int, name: String)
    case class Location(id: Int, address: String)
    
    val json = Json.obj("id" -> 1, "name" -> "John", "address"-> "Paris", "contact" -> "1234")
    
    implicit val pReads = Json.reads[Person]
    implicit val lReads = Json.reads[Location]
    
    assert { withExtra[Person].reads(json).get == (
      Person(1, "John"),
      Json.obj("address"-> "Paris", "contact" -> "1234")
    ) }
    
    assert { withExtra[Location].reads(json).get == (
      Location(1, "Paris"),
      Json.obj("name" -> "John", "contact" -> "1234")
    ) }
    

    Runnable code is available there