Как манипулировать 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)
    }
  }
}

Мне это не нравится по многим причинам:

  1. Приходится явно бросать результаты parse(json) в JObject
  2. Результат upsertField функция является JValue, который мне придется переделывать, если я хочу манипулировать объектом дальше
  3. upsertField функция просто чувствует себя очень не элегантно
  4. Он не работает для полей, которые не находятся на верхнем уровне иерархии

Есть ли лучший способ трансформировать АСТ?

РЕДАКТИРОВАТЬ: как обходной путь к проблеме, мне удалось преобразовать мой 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-классов с неявными переопределениями, автономной библиотекой.

Другие вопросы по тегам