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

1.5k views Asked by At

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 ?

1

There are 1 answers

0
Oleg Pyzhcov On BEST ANSWER

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