andrey.ladniy
andrey.ladniy

Reputation: 1674

Add optional property with json transformers in playframework 2.4

I can't understand, how can i add optional property with json transformer.

I want merge two json objects (list and calendars) without or with dynamic list of properties (for example without owner):

calendar1 = {id:1, name: "first", description:"my first calendar", owner: 1}
calendar2 = {id:2, name: "second", owner: 1}

list = [{id: 1, settings: []}, {id: 2, settings: []}]

and result must be

{calendars:
    [
      {id:1, name: "first", description:"my first calendar", settings: []}, 
      {id:2, name: "second", settings: []}
    ]
}

Upvotes: 1

Views: 462

Answers (3)

andrey.ladniy
andrey.ladniy

Reputation: 1674

Many thanks to @Jean and comments in "Unveiling Play 2.1 Json API - Part 3 : JSON Transformers"

It's hard understanding things like and, andThen, andKeep, keepAnd in JSON transformers for me (I can not find any detailed descriptions with examples), but i found some templates for my question:

Optional property in JSON:

With Reader

(__ \ "id").json.pick[JsString].flatMap{
  case id if id.equals(JsString(accountId)) =>
    (__ \ "primary").json.put(JsBoolean(true))
  case _ =>
    Reads.pure(Json.obj())
}

With Json.obj()

(__ \ "id").json.pick[JsString].map{
  case id if id.equals(JsString(accountId)) =>
    Json.obj("primary" -> true)
  case _ =>
    Json.obj()
}

Upvotes: 0

Jean
Jean

Reputation: 21595

I'll assume the following json trees

val calendar1 = Json.parse("""{"id":1, "name": "first", "description":"my first calendar", "owner": 1}""")
val calendar2 = Json.parse("""{"id":2, "name": "second", "owner": 1}""")

You need to add settings to each calendar, then remove the owner if it exists.

Putting a value in branch settings is explained in the documentation

val addSettings = __.json.update((__ \ "settings").json.put(Json.arr()))

Dropping the owner is also explained

val removeOwner = (__ \ "owner").json.prune

Now you can define the transformer to be applied to each of your calendar object

val transformer = addSettings andThen removeOwner

With that in place there are multiple options depending on how your data is actually modeled. If you have a Seq of calendars as in

val calendars = Seq(calendar1, calendar2)

you can do

val normalizedCalendars = calendars.map(_.transform(transformer)) 

This gives you a Seq[JsResult[JsObject]] which you want to transform into a JsResult[Seq[JsObject]].

I am pretty sure there is a way to do it using play's functional syntax (see play.api.libs.functional and play.api.libs.functional.syntax) but this part of play is not well documented and I haven't gotten around to studying Applicatives yet even though I have a feel for what they do.

Instead, I rely on the following code inspired by scala's Future#sequence

def sequence[A, M[X] <: TraversableOnce[X]](in: M[JsResult[A]])(implicit cbf: CanBuildFrom[M[JsResult[A]], A, M[A]]): JsResult[M[A]] = {
  val empty: JsResult[mutable.Builder[A, M[A]]] = JsSuccess(cbf(in))
  in.foldLeft(empty) {(jracc,jrel) => (jracc,jrel) match {
      case (JsSuccess(builder,_), JsSuccess(a,p)) =>JsSuccess(builder+=a, p)
      case (ra@JsError(builderErrors), re@JsError(errors)) =>JsError.merge(ra, re)
      case (JsSuccess(_,_), re@JsError(errors)) => re
      case (ra@JsError(builderErrors), JsSuccess(_,_)) => ra
    }} map (_.result())
}

With that you can write :

val calendarArray = sequence(normalizedCalendars).map(v=>Json.obj("calendars"->JsArray(v)))

which will give you a JsResult[JsObject]. As long as your original calendars are indeed JsObjects you will get a JsSuccess. You can verify the output structure with :

calendarArray.foreach(println)

which returns :

{"calendars":[{"id":1,"name":"first","description":"my first calendar","settings":[]},{"id":2,"name":"second","settings":[]}]}

which is the same as what you asked modulo some whitespace

{
  "calendars":[
    {"id":1,"name":"first","description":"my first calendar","settings":[]},
    {"id":2,"name":"second","settings":[]}
  ]
}

Upvotes: 1

Andrzej Jozwik
Andrzej Jozwik

Reputation: 14649

Start with:

scala> case class Calendar(id:Int,name:String,description:Option[String],owner:Int)
defined class Calendar

scala> case class CalendarRow(id:Int,name:String,description:Option[String],settings:Seq[String]=Seq.empty)
defined class CalendarRow

scala>  def append(calendars:Calendar*) = calendars.map(c => CalendarRow(c.id,c.name,c.description))
append: (calendars: Calendar*)Seq[CalendarRow]

scala> val calendar1 = Calendar(1,"first",Option("my first calendar"),1)
calendar1: Calendar = Calendar(1,first,Some(my first calendar),1)

scala> val calendar2 = Calendar(2, "second",None,1)
calendar2: Calendar = Calendar(2,second,None,1)

scala> val list =  append(calendar1,calendar2)
list: Seq[CalendarRow] = ArrayBuffer(CalendarRow(1,first,Some(my first calendar),List()), CalendarRow(2,second,None,List()))

Upvotes: 0

Related Questions