Подтвердить что ты не робот

Scala/Воспроизвести: разобрать JSON в Map вместо JsObject

На домашней странице Play Framework они утверждают, что "JSON является гражданином первого класса". Мне еще предстоит доказать это.

В моем проекте я имею дело с довольно сложными структурами JSON. Это просто очень простой пример:

{
    "key1": {
        "subkey1": {
            "k1": "value1"
            "k2": [
                "val1",
                "val2"
                "val3"
            ]
        }
    }
    "key2": [
        {
            "j1": "v1",
            "j2": "v2"
        },
        {
            "j1": "x1",
            "j2": "x2"
        }
    ]
}

Теперь я понимаю, что Play использует Jackson для разбора JSON. Я использую Jackson в своих проектах Java, и я бы сделал что-то простое:

ObjectMapper mapper = new ObjectMapper();
Map<String, Object> obj = mapper.readValue(jsonString, Map.class);

Это хорошо проанализировало бы мой JSON в объекте Map, который я хочу - Карта строк и объектов, и позволил бы мне легко отличить массив от ArrayList.

Тот же пример в Scala/Play будет выглядеть следующим образом:

val obj: JsValue = Json.parse(jsonString)

Это вместо этого дает мне проприетарный JsObject тип, который на самом деле не тот, что я за ним.

Мой вопрос: могу ли я разбирать строку JSON в Scala/Play до Map вместо JsObject так же легко, как и в Java?

Боковой вопрос: есть ли причина, по которой JsObject используется вместо Map в Scala/Play?

Мой стек: Play Framework 2.2.1/ Scala 2.10.3/Java 8 64bit/Ubuntu 13.10 64bit

UPDATE: Я вижу, что ответ Трэвиса поддерживается, поэтому я думаю, что это имеет смысл для всех, но я все еще не понимаю, как это можно применить для решения моей проблемы. Скажем, у нас есть этот пример (jsonString):

[
    {
        "key1": "v1",
        "key2": "v2"
    },
    {
        "key1": "x1",
        "key2": "x2"
    }
]

Ну, по всем направлениям, теперь я должен включить все, что шаблон, что я иначе не понимаю цель:

case class MyJson(key1: String, key2: String)
implicit val MyJsonReads = Json.reads[MyJson]
val result = Json.parse(jsonString).as[List[MyJson]]

Выглядит хорошо, да? Но подождите минуту, в массив входит еще один элемент, который полностью разрушает этот подход:

[
    {
        "key1": "v1",
        "key2": "v2"
    },
    {
        "key1": "x1",
        "key2": "x2"
    },
    {
        "key1": "y1",
        "key2": {
            "subkey1": "subval1",
            "subkey2": "subval2"
        }
    }
]

Третий элемент больше не соответствует моему определенному классу case - я снова на квадрате. Я могу использовать такие и гораздо более сложные структуры JSON на Java каждый день, а Scala предполагает, что я должен упростить свои JSON, чтобы он соответствовал политике типа "безопасный тип". Исправьте меня, если я ошибаюсь, но я, хотя этот язык должен служить данным, а не наоборот?

UPDATE2: Решение состоит в том, чтобы использовать модуль Jackson для Scala (пример в моем ответе).

4b9b3361

Ответ 1

Я выбрал модуль Jackson для scala.

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper

val mapper = new ObjectMapper() with ScalaObjectMapper
mapper.registerModule(DefaultScalaModule)
val obj = mapper.readValue[Map[String, Object]](jsonString)

Ответ 2

Scala в целом препятствует использованию downcasting, и Play Json является идиоматичным в этом отношении. Downcasting - проблема, потому что это делает невозможным компилятор, чтобы помочь вам отслеживать возможность недействительных ввода или других ошибок. После того, как вы получили значение типа Map[String, Any], вы сами по себе - компилятор не может помочь вам отслеживать значения этих Any.

У вас есть несколько альтернатив. Первый заключается в использовании операторов пути для перехода к определенной точке в дереве, где вы знаете тип:

scala> val json = Json.parse(jsonString)
json: play.api.libs.json.JsValue = {"key1": ...

scala> val k1Value = (json \ "key1" \ "subkey1" \ "k1").validate[String]
k1Value: play.api.libs.json.JsResult[String] = JsSuccess(value1,)

Это похоже на следующее:

val json: Map[String, Any] = ???

val k1Value = json("key1")
  .asInstanceOf[Map[String, Any]]("subkey1")
  .asInstanceOf[Map[String, String]]("k1")

Но прежний подход имеет преимущество в том, что он проваливается путями, которые легче рассуждать. Вместо потенциально сложного для интерпретации исключения ClassCastException мы получили бы простое значение JsError.

Обратите внимание, что мы можем проверить в точке, более высокой в ​​дереве, если мы знаем, какую структуру мы ожидаем:

scala> println((json \ "key2").validate[List[Map[String, String]]])
JsSuccess(List(Map(j1 -> v1, j2 -> v2), Map(j1 -> x1, j2 -> x2)),)

Оба этих примера воспроизведения основаны на концепции классов типов и, в частности, на экземплярах класса типа Read, предоставляемых Play. Вы также можете предоставить собственные экземпляры классов типов для типов, которые вы сами определили. Это позволит вам сделать что-то вроде следующего:

val myObj = json.validate[MyObj].getOrElse(someDefaultValue)

val something = myObj.key1.subkey1.k2(2)

Или что угодно. Документация Play (связанная выше) дает хорошее представление о том, как это сделать, и вы всегда можете задавать следующие вопросы здесь, если у вас возникают проблемы.


Чтобы устранить обновление в вашем вопросе, вы можете изменить свою модель, чтобы учесть различные возможности для key2, а затем определить свой собственный экземпляр Reads:

case class MyJson(key1: String, key2: Either[String, Map[String, String]])

implicit val MyJsonReads: Reads[MyJson] = {
  val key2Reads: Reads[Either[String, Map[String, String]]] =
    (__ \ "key2").read[String].map(Left(_)) or
    (__ \ "key2").read[Map[String, String]].map(Right(_))

  ((__ \ "key1").read[String] and key2Reads)(MyJson(_, _))
}

Что работает следующим образом:

scala> Json.parse(jsonString).as[List[MyJson]].foreach(println)
MyJson(v1,Left(v2))
MyJson(x1,Left(x2))
MyJson(y1,Right(Map(subkey1 -> subval1, subkey2 -> subval2)))

Да, это немного больше подробностей, но это передняя многословие, которую вы платите за один раз (и это дает вам несколько хороших гарантий), а не кучу приведений, которые могут привести к запутыванию ошибок времени выполнения.

Это не для всех, и это может быть не по вашему вкусу - это прекрасно. Вы можете использовать операторы пути для обработки таких случаев, или даже простой старый Джексон. Я бы посоветовал вам придать типу классного подхода шанс, хотя есть крутая кривая обучения, но многие люди (включая меня) очень сильно предпочитают это.

Ответ 3

Для дополнительной справки и в духе простоты вы всегда можете пойти:

Json.parse(jsonString).as[Map[String, JsValue]]

Однако это вызовет исключение для строк JSON, не соответствующих формату (но я предполагаю, что это касается и подхода Джексона). Теперь JsValue можно обработать следующим образом:

jsValueWhichBetterBeAList.as[List[JsValue]]

Я надеюсь, что разница между обработкой Object и JsValue не является проблемой для вас (только потому, что вы жаловались на JsValue, являющийся проприетарным). Очевидно, что это немного похоже на динамическое программирование на типизированном языке, что обычно не в том, чтобы идти (ответ Тревиса обычно - путь), но иногда это приятно, если я предполагаю.

Ответ 4

Вы можете просто извлечь значение Json, а scala - соответствующую карту. Пример:

   var myJson = Json.obj(
          "customerId" -> "xyz",
          "addressId" -> "xyz",
          "firstName" -> "xyz",
          "lastName" -> "xyz",
          "address" -> "xyz"
      )

Предположим, что у вас есть Json из вышеприведенного типа. Чтобы преобразовать его в карту, просто выполните:

var mapFromJson = myJson.value

Это дает вам карту типа: scala.collection.immutable.HashMap $HashTrieMap

Ответ 5

Порекомендовал бы читать информацию о совпадении шаблонов и рекурсивных ADT в целом, чтобы лучше понять, почему Play Json рассматривает JSON как "гражданина первого класса".

При этом многие API-интерфейсы Java (например, библиотеки Java) ожидают, что JSON будет десериализован как Map[String, Object]. Хотя вы можете просто создать свою собственную функцию, которая рекурсивно генерирует этот объект с помощью сопоставления с образцом, самым простым решением, вероятно, будет использование следующего существующего шаблона:

import com.google.gson.Gson
import java.util.{Map => JMap, LinkedHashMap}

val gson = new Gson()

def decode(encoded: String): JMap[String, Object] =   
   gson.fromJson(encoded, (new LinkedHashMap[String, Object]()).getClass)

LinkedHashMap используется, если вы хотите сохранить порядок клавиш во время десериализации (HashMap можно использовать, если упорядочение не имеет значения). Полный пример здесь.