How to merge a JsValue to JsObject in flat level
Asked Answered
S

2

14

I have two JsValue created from case class, i.e. Book and Book detail

val bookJson = Json.tojson(Book)
val bookDetailJson = Json.tojson(BookDetail)

and the format would be:

//Book
{
  id: 1,
  name: "A Brief History of Time"
}

//BookDetail
{
  bookId: 1,
  author: "Steven Hawking",
  publicationDate: 1988,
  pages: 256
}

How can I merge them to a single Json in play-framework 2.10? i.e.

//Book with detail
{
  id: 1,
  name: "A Brief History of Time",
  bookId: 1,
  author: "Steven Hawking",
  publicationDate: 1988,
  pages: 256
}

I was trying the transformation and failed to iterate through the second JsValue:

val mapDetail = (__).json.update(
                  __.read[JsObject].map { o =>
                  o.deepMerge( JsObject(Seq(("detail", bookDetailJson))) )
                })

bookJson.validate(mapDetail).get

It would become one level down, which I don't really want.

//Book with detail
{
  id: 1,
  name: "A Brief History of Time",
  detail: {
            bookId: 1,
            author: "Steven Hawking",
            publicationDate: 1988,
            pages: 256
          }
}

Please let me know if any trick could provide on this Json transform. Many Thanks!

Swamper answered 11/7, 2013 at 14:58 Comment(7)
It looks like the JsObject class supports the ++ method for combining two JsObjects. Have you tried casting them to JsObject (if they already are not that type) and then using ++?Hoahoactzin
That was my frist attempt which gave me the same result. Indeed, I thought deepMerge would give me a better answer.Swamper
The real problem is when I converting a JsValue to JsObject, I can only use JsObject(Seq(("details", detailsJson))) which would generate this: detail: { bookId: 1, author: "Steven Hawking", publicationDate: 1988, pages: 256 }Swamper
Again, if you cast both to JsObject, you should be able to copy the fields from one into the other like so: val newObj = a.copy(fields = fields ++ b.fields).Hoahoactzin
Sorry for trivial question. What's the best way to cast a JsValue to JsObject?Swamper
It's totally ugly, but jvalue.asInstanceOf[JObject] where jvalue is the JValue you want to convertHoahoactzin
Thanks cmbaxter! Indeed, that work for : val detailObj = detailJson.asInstanceOf[JsObject] val mapDetail = (__).json.update( __.read[JsObject].map { o => o ++ detailObj })Swamper
T
18

Play has a lot of new features for JSON right now. This would be a nice showcase for the Format[A] trait (see Scala Json Inception) which you could include implicitly as I will show, or explicitly to the methods that require an implicit Format[A]/Reads[A]/Writes[A].

Create a case class to represent your JSON objects,

case class Book(id: Int, name: String)
case class BookDetail(id: Int, author: String, publicationDate: Int, pages: Int)

Create companion objects that contain the implicit Format[A] so that Format/Reads/Writes will automatically be in scope when you need them.

object Book { 
  implicit val fmt: Format[Book] = Json.format[Book] 
}

object BookDetail { 
  implicit val fmt: Format[BookDetail] = Json.format[BookDetail] 
}

Now you could do something like this,

val bookJson = Json.toJson(Book(1, "A Brief History Of Time"))
val bookDetailJson = Json.toJson(BookDetail(1, "Steven Hawking", 1988, 256))
bookJson.as[JsObject].deepMerge(bookDetailJson.as[JsObject])

And you will have an object like this,

{
  id: 1,
  name: "A Brief History Of Time",
  author: "Steven Hawking",
  publicationDate: 1988,
  pages: 256
}

I've tried this in the REPL but it does not work, in a Play application it does just fine though. Also in a production scenario we would likely use asOpt[T] in place of as[T].

Here is an example of why asOpt[T] may be better suited, suppose instead of a valid JSON object for book you get,

val bookJson = Json.toJson("not a book")

You will end up with a

[JsResultException: JsResultException(errors:List((,List(ValidationError(validate.error.expected.jsobject,WrappedArray())))))]

But suppose instead you change your method to use asOpt[T],

bookJson.asOpt[JsObject].getOrElse(Json.obj()).deepMerge(bookDetailJson.asOpt[JsObject].getOrElse(Json.obj()))

Now you will end up with at least a partial JSON object,

{
  id: 1,
  author: "Steven Hawking",
  publicationDate: 1988,
  pages: 256
}

So depending on how you would like to handle improperly formatted JSON you could choose either option.

Tangle answered 23/10, 2013 at 20:3 Comment(1)
+1 Thanks soo much deepMerge was exactly what I was looking for but the Scala api is so big that its easy to overlook!Religious
S
2

JsObject is subtype of JsValue.

JsValue can be simple converted to the JsObject using as or asOpt methods from JsValue. Example:

val someJsValue = ....
val asObject:JsObject = someJsValue.as[JsObject]
val asObjectMaybe:Option[JsObject] = v.asOpt[JsObject]

In the case of JsArray you can not use above code. If you use play and parse JSON with array, then Json.toJson(...) produces JsValue which is JsArray actually. You need to convert JsArray as following:

val someJsValueButArray = ....
val asJsArray:JsArray = Json.toJson(someJsValueButArray).as[JsArray]
val asSeqOfJsObjects:Seq[JsObject] = asJsArray.value.map(_.as[JsObject])
Shirl answered 22/4, 2016 at 14:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.