Как объединить JsValue в JsObject на ровном уровне

У меня есть два JsValue, созданных из класса case, то есть Book and Book detail

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

и формат будет:

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

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

Как я могу объединить их с одним Json в play-framework 2.10? т.е.

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

Я пытался преобразовать и не смог повторить второй JsValue:

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

bookJson.validate(mapDetail).get

Это станет на один уровень ниже, чего я действительно не хочу.

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

Пожалуйста, дайте мне знать, может ли любой трюк обеспечить это преобразование Json. Большое спасибо!

Ответ 1

В настоящее время у Play есть много новых возможностей для JSON. Это была бы приятная витрина для свойства Format[A] (см. Scala Json Inception), которую вы могли бы включить неявно, как я покажу, или явно для методов, которым требуется неявный Format[A]/Reads[A]/Writes[A].

Создайте класс case для представления ваших объектов JSON,

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

Создайте объекты сопутствующих объектов, которые содержат неявный Format[A] так что Format/Reads/Writes автоматически попадет в область действия по мере необходимости.

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

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

Теперь вы можете сделать что-то подобное,

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])

И у вас будет такой объект,

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

Я пробовал это в REPL, но это не сработает, в Play Play оно все равно прекрасно. Также в сценарии производства мы, скорее всего, будем использовать asOpt[T] вместо as[T].

Вот пример того, почему asOpt[T] может быть лучше подходит, предположим вместо действительного объекта JSON для книги, которую вы получаете,

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

В итоге вы получите

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

Но предположим, что вместо этого вы измените свой метод на использование asOpt[T],

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

Теперь у вас будет, по крайней мере, частичный объект JSON,

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

Таким образом, в зависимости от того, как вы хотели бы обрабатывать неправильно отформатированный JSON, вы можете выбрать любой вариант.

Ответ 2

JsObject - это подтип JsValue.

JsValue может быть просто преобразован в JsObject, используя методы asopt из JsValue. Пример:

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

В случае JsArray вы не можете использовать код выше. Если вы используете play и разбираете JSON с массивом, Json.toJson(...) создает JsValue, который на самом деле является JsArray. Вам нужно преобразовать JsArray следующим образом:

val someJsValueButArray = ....
val asJsArray:JsArray = Json.toJson(someJsValueButArray).as[JsArray]
val asSeqOfJsObjects:Seq[JsObject] = asJsArray.value.map(_.as[JsObject])