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 (пример в моем ответе).
Ответы
Ответ 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 можно использовать, если упорядочение не имеет значения). Полный пример здесь.