Krešimir Nesek
Krešimir Nesek

Reputation: 5492

Modifying JSON reads and writes in playframework 2.1

I'm a newbie and scala/play and need help with playframework's JSON reads/writes.

I use Json.reads[T] and Json.writes[T] macros to define json reads and writes for my classes. However I'd like to have one property name to be (always) mapped differently. Namely, I have property named id in my classes and I want it to be represented as _id when object is converted to json and vice versa.

Is there a way to modify reads/writes objects generated by Json.reads and Json.writes macros to achieve this or do I have to rewrite reads and writes manually just to have one property named differently?

EDIT

Let me try to explain the problem better. Consider the model object User:

case class User (id: BigInt, email: String, name: String)

When serializing User to json for purposes of serving json in context of a REST api the json should look like this:

{ "id": 23432, "name": "Joe", "email: "[email protected]" }

When serializing User to json for purposes of storing/updating/reading form MongoDB json should look like:

{ "_id": 23432, "name": "Joe", "email: "[email protected]" }

In other words everything is the same except when communicating with Mongo id should be represented as _id.

I know I could manually write two sets of reads and writes for each model object (one to be used for web and another for communication with Mongo) as suggested by Darcy Qiu in the answer, however maintaining two sets of reads and writes that are nearly identical except for the id property seems like a lot of code duplication so I'm wondering if there is a better approach.

Upvotes: 1

Views: 2161

Answers (3)

tehshy
tehshy

Reputation: 108

First you define transformations for renames id/_id back and forth:

import play.api.libs.json._
import play.modules.reactivemongo.json._

val into: Reads[JsObject] = __.json.update( // copies the full JSON
  (__ \ 'id).json.copyFrom( (__ \ '_id).json.pick ) // adds id
) andThen (__ \ '_id).json.prune  // and after removes _id

val from: Reads[JsObject] = __.json.update( // copies the full JSON
  (__ \ '_id).json.copyFrom( (__ \ 'id).json.pick ) // adds _id
) andThen (__ \ 'id).json.prune  // and after removes id

(To understand why Reads is a transformation please read: https://www.playframework.com/documentation/2.4.x/ScalaJsonTransformers)

Assuming we have macro generated Writes and Reads for our entity class:

def entityReads: Reads[T] // eg Json.reads[Person]
def entityWrites: Writes[T] // eg Json.writes[Person]

Then we mix transformations with macro-generated code:

private[this] def readsWithMongoId: Reads[T] = 
  into.andThen(entityReads)
private[this] def writesWithMongoId: Writes[T] =
  entityWrites.transform(jsValue => jsValue.transform(from).get)

The last thing. Mongo driver wants to be sure (ie typesafe-sure) that the json it inserts is a JsObject. That is why we need an OWrites. I haven't found better way than:

private[this] def oWritesWithMongoId = new OWrites[T] {
  override def writes(o: T): JsObject = writesWithMongoId.writes(o) match {
    case obj: JsObject => obj
    case notObj: JsValue => 
      throw new InternalError("MongoRepo has to be" +
      "definded for entities which serialize to JsObject")
  }

}

Last step is to privide an implicit OFormat.

implicit val routeFormat: OFormat[T] = OFormat(
  readsWithMongoId,
  oWritesWithMongoId
)

Upvotes: 1

Klaus Baumgartner
Klaus Baumgartner

Reputation: 128

If you put in enough code you can achieve this with transformers:

val idToUnderscore = (JsPath).json.update((JsPath).read[JsObject].map { o:JsObject =>
  o ++ o.transform((JsPath\"_id").json.put((JsPath\"id").asSingleJson(o))).get
}) andThen (JsPath\"id").json.prune
val underscoreWrite = normalWrites.transform( jsVal => jsVal.transform(idToUnderscore).get )

Here's the full test:

import play.api.libs.functional.syntax._
import play.api.libs.json._

val u = User("overlord", "Hansi Meier", "[email protected]")

val userToProperties = {u:User => (u.id, u.name, u.email)}
val normalWrites = (
   (JsPath\"id").write[String] and
   (JsPath\"name").write[String] and
   (JsPath\"email").write[String]
 )(userToProperties)

val idToUnderscore = (JsPath).json.update((JsPath).read[JsObject].map { o:JsObject =>
  o ++ o.transform((JsPath\"_id").json.put((JsPath\"id").asSingleJson(o))).get
}) andThen (JsPath\"id").json.prune
val underscoreWrite = normalWrites.transform( jsVal => jsVal.transform(idToUnderscore).get )

info(Json.stringify(Json.toJson(u)(normalWrites)))
info(Json.stringify(Json.toJson(u)(underscoreWrite)))

Now, if you modify the normalWrites (say by adding additional properties), the underscoreWrite will still do what you want.

Upvotes: 0

darcyy
darcyy

Reputation: 5276

Let's say your case class, which is T in your question, is named User and has definision as below

case class User(_id: String, age: Int)

Your reads can be defined as

implicit val userReads = new Reads[User] {
  def reads(js: JsValue): User = {
    User(
      (js \ "id").as[String],
      (js \ "age").as[Int]
    )
  }
}

Your writes[User] should follow the same logic.

Upvotes: 0

Related Questions