Как манипулировать JSON AST в Scala
Я экспериментирую с библиотекой json4s (основанной на lift-json). Одна из вещей, которую я хотел бы сделать, - это проанализировать строку JSON в AST, а затем манипулировать ею.
Например, я хотел бы добавить поле (вставить поле в AST, если оно не существует, или обновить его значение, если оно существует).
Я не смог найти, как это сделать в документации. Экспериментируя с доступными методами, я придумал следующее, которое работает, но кажется неуклюжим.
import org.json4s._
import org.json4s.JsonDSL._
import org.json4s.native.JsonMethods._
object TestJson {
implicit val formats = DefaultFormats
def main(args: Array[String]): Unit = {
val json = """{"foo":1, "bar":{"foo":2}}"""
val ast = parse(json).asInstanceOf[JObject]
println( upsertField(ast, ("foo" -> "3")) )
println( upsertField(ast, ("foobar" -> "3")) )
}
def upsertField(src:JObject, fld:JField): JValue = {
if(src \ fld._1 == JNothing){
src ~ fld
}
else{
src.replace(List(fld._1), fld._2)
}
}
}
Мне это не нравится по многим причинам:
- Приходится явно бросать результаты
parse(json)
вJObject
- Результат
upsertField
функция являетсяJValue
, который мне придется переделывать, если я хочу манипулировать объектом дальше upsertField
функция просто чувствует себя очень не элегантно- Он не работает для полей, которые не находятся на верхнем уровне иерархии
Есть ли лучший способ трансформировать АСТ?
РЕДАКТИРОВАТЬ: как обходной путь к проблеме, мне удалось преобразовать мой JSON в обычные классы Scala и управлять ими с помощью линз ( Использование линз в регулярных классах Scala)
3 ответа
Существует функция слияния, которая создает или переопределяет поле. Вы также можете обновить поля, которые не находятся на корневом уровне дерева.
import org.json4s._
import org.json4s.JsonDSL._
import org.json4s.jackson.JsonMethods._
object mergeJson extends App {
val json =
"""
|{
| "foo":1,
| "bar": {
| "foo": 2
| }
|}
|""".stripMargin
val ast = parse(json)
val updated = ast merge (("foo", 3) ~ ("bar", ("fnord", 5)))
println(pretty(updated))
// {
// "foo" : 3,
// "bar" : {
// "foo" : 2,
// "fnord" : 5
// }
// }
}
Позвольте мне также дать вам версию SON of JSON:
import nl.typeset.sonofjson._
val json = parse("""{ "foo" : 1, "bar" : { "foo" : 2 } }""")
// or, perhaps a little easier
val json = obj(foo = 1, bar = obj(foo = 2))
json.foo = "3"
json.foobar = "3"
Когда я реализовывал какой-то очень специфический json diff с использованием lift json, я использовал множество рекурсивных функций, чтобы добраться до jpath, где мне нужно изменить значение, и модифицированный json был создан, когда рекурсия "свернулась". LiftJson неизменен в конце концов. Вы упомянули линзы как еще один подход, который сам по себе очень интересен. Но в настоящее время мой любимый вариант - библиотека play-json, которая работает как шарм в ситуации, когда вам нужно выполнить преобразование json-в-json:
из блога Mandubian:
val gizmo2gremlin = (
(__ \ 'name).json.put(JsString("gremlin")) and
(__ \ 'description).json.pickBranch(
(__ \ 'size).json.update( of[JsNumber].map{ case JsNumber(size) => JsNumber(size * 3) } ) and
(__ \ 'features).json.put( Json.arr("skinny", "ugly", "evil") ) and
(__ \ 'danger).json.put(JsString("always"))
reduce
) and
(__ \ 'hates).json.copyFrom( (__ \ 'loves).json.pick )
) reduce
Вкусные функции: все преобразователи являются комбинаторами, которые можно смешивать вместе, валидацией, бесформенной поддержкой, автоматическим маршалингом case-классов с неявными переопределениями, автономной библиотекой.