Рекурсивно преобразуйте дерево JSON в другой формат (XML, CSV и т. Д.) С помощью circe - PullRequest
3 голосов
/ 09 марта 2019

Чтобы преобразовать узлы JSON в другой формат, чем JSON (например, XML, CSV и т. Д.) С помощью Circe, я нашел решение, в котором мне нужно было получить доступ к внутренним структурам данных Circe.

Это мой рабочий пример, который преобразует JSON в строку XML (не идеально, но вы поняли):

package io.circe

import io.circe.Json.{JArray, JBoolean, JNull, JNumber, JObject, JString}
import io.circe.parser.parse

object Sample extends App {

  def transformToXMLString(js: Json): String = js match {
    case JNull => ""
    case JBoolean(b) => b.toString
    case JNumber(n) => n.toString
    case JString(s) => s.toString
    case JArray(a) => a.map(transformToXMLString(_)).mkString("")
    case JObject(o) => o.toMap.map {
      case (k, v) => s"<${k}>${transformToXMLString(v)}</${k}>"
    }.mkString("")
  }

  val json =
    """{
      | "root": {
      |  "sampleboolean": true,
      |  "sampleobj": {
      |    "anInt": 1,
      |    "aString": "string"
      |  },
      |  "objarray": [
      |     {"v1": 1},
      |     {"v2": 2}
      |  ]
      | }
      |}""".stripMargin

  val res = transformToXMLString(parse(json).right.get)
  println(res)
}

Результат:

<root><sampleboolean>true</sampleboolean><sampleobj><anInt>1</anInt><aString>string</aString></sampleobj><objarray><v1>1</v1><v2>2</v2></objarray></root>

Это нормально, если низкоуровневые объекты JSON (например, JBoolean, JString, JObject и т. Д.) Не были пакетом private в circe, что делает приведенный выше код работающим, только если он помещен в пакет package io.circe.

Как вы можете достичь того же результата, что и выше, используя публичный Circe API?

Ответы [ 2 ]

5 голосов
/ 09 марта 2019

Метод fold в Json позволяет вам выполнять эту операцию довольно кратко (и таким образом, чтобы обеспечить исчерпывающую способность, точно так же, как сопоставление с образцом на запечатанной характеристике):

import io.circe.Json

def transformToXMLString(js: Json): String = js.fold(
  "",
  _.toString,
  _.toString,
  identity,
  _.map(transformToXMLString(_)).mkString(""),
  _.toMap.map {
    case (k, v) => s"<${k}>${transformToXMLString(v)}</${k}>"
  }.mkString("")
)

И затем:

scala> import io.circe.parser.parse
import io.circe.parser.parse

scala> transformToXMLString(parse(json).right.get)
res1: String = <root><sampleboolean>true</sampleboolean><sampleobj><anInt>1</anInt><aString>string</aString></sampleobj><objarray><v1>1</v1><v2>2</v2></objarray></root>

Точно такой же результат, как и у вашей реализации, но с меньшим количеством символов и без учета личных деталей реализации.

Таким образом, ответ «use * 1011»* "(или asX методы, предложенные в другом ответе - этот подход более гибок, но в целом, вероятно, будет менее идиоматичным и более многословным).Если вам не безразлично, почему мы приняли решение о проектировании в Цирцее, чтобы не показывать конструкторы, вы можете перейти к концу этого ответа, но этот вопрос часто возникает, поэтому я также хочу затронуть несколько связанных с этим вопросов.first.

Примечание по поводу именования

Обратите внимание, что использование имени "fold" для этого метода унаследовано от Argonaut и, возможно, неточно.Когда мы говорим о катаморфизмах (или сгибах) для рекурсивных алгебраических типов данных, мы имеем в виду функцию, в которой мы не видим тип ADT в аргументах передаваемых нами функций. Например, сигнатура сгиба для списковвыглядит следующим образом:

def foldLeft[B](z: B)(op: (B, A) => B): B

Не так:

def foldLeft[B](z: B)(op: (List[A], A) => B): B

Поскольку io.circe.Json является рекурсивным ADT, его метод fold действительно должен выглядеть следующим образом:

def properFold[X](
  jsonNull: => X,
  jsonBoolean: Boolean => X,
  jsonNumber: JsonNumber => X,
  jsonString: String => X,
  jsonArray: Vector[X] => X,
  jsonObject: Map[String, X] => X
): X

Вместо:

def fold[X](
  jsonNull: => X,
  jsonBoolean: Boolean => X,
  jsonNumber: JsonNumber => X,
  jsonString: String => X,
  jsonArray: Vector[Json] => X,
  jsonObject: JsonObject => X
): X

Но на практике первое кажется менее полезным, поэтому Цирцея предоставляет только второе (если вы хотите выполнить повторение, вы должны сделать это вручную), и следует за Аргонавтом вназывая это fold.Это всегда заставляло меня чувствовать себя немного неловко, и имя может измениться в будущем.

Дополнительное замечание о производительности

В некоторых случаях создание шести функций, ожидаемых fold, может быть чрезмерно дорогимТаким образом, circe также позволяет объединять операции вместе:

import io.circe.{ Json, JsonNumber, JsonObject }

val xmlTransformer: Json.Folder[String] = new Json.Folder[String] {
    def onNull: String = ""
  def onBoolean(value: Boolean): String = value.toString
  def onNumber(value: JsonNumber): String = value.toString
  def onString(value: String): String = value
  def onArray(value: Vector[Json]): String =
    value.map(_.foldWith(this)).mkString("")
  def onObject(value: JsonObject): String = value.toMap.map {
    case (k, v) => s"<${k}>${transformToXMLString(v)}</${k}>"
  }.mkString("")
}

И затем:

scala> parse(json).right.get.foldWith(xmlTransformer)
res2: String = <root><sampleboolean>true</sampleboolean><sampleobj><anInt>1</anInt><aString>string</aString></sampleobj><objarray><v1>1</v1><v2>2</v2></objarray></root>

Выигрыш в производительности от использования Folder будет зависеть от того, включен ли вы2.11 или 2.12, но если фактические операции, выполняемые над значениями JSON, являются дешевыми, можно ожидать, что версия Folder получит примерно вдвое большую пропускную способность, чем fold.Между прочим, это также значительно быстрее, чем сопоставление с образцом на внутренних конструкторах, по крайней мере в тестах, которые мы сделали :

Benchmark                           Mode  Cnt      Score    Error  Units
FoldingBenchmark.withFold          thrpt   10   6769.843 ± 79.005  ops/s
FoldingBenchmark.withFoldWith      thrpt   10  13316.918 ± 60.285  ops/s
FoldingBenchmark.withPatternMatch  thrpt   10   8022.192 ± 63.294  ops/s

Это на 2.12.Я полагаю, что вы должны увидеть еще большую разницу в 2.11.

Дополнительное замечание по поводу оптики

Если вы действительно хотите сопоставление с образцом, circe-optics дает вам высокую оценкуальтернатива экстракторам класса case:

import io.circe.Json, io.circe.optics.all._

def transformToXMLString(js: Json): String = js match {
    case `jsonNull` => ""
  case jsonBoolean(b) => b.toString
  case jsonNumber(n) => n.toString
  case jsonString(s) => s.toString
  case jsonArray(a) => a.map(transformToXMLString(_)).mkString("")
  case jsonObject(o) => o.toMap.map {
    case (k, v) => s"<${k}>${transformToXMLString(v)}</${k}>"
  }.mkString("")
}

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

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

Почему бы не простотематические классы

Когда я впервые начал работать с Circe, я написал следующее в документ о некоторых из моих проектных решений :

В некоторых случаях, включая большинствов данном случае тип io.circe.Json, мы не хотим, чтобы пользователи думали, что ADT-листы имеют значимые типы.Значение JSON «является» логическим значением, либо строкой, либо единицей, либо Seq[Json], либо JsonNumber, либо JsonObject.Введение таких типов, как JString, JNumber и т. Д., В общедоступный API просто сбивает с толку.

Мне нужен действительно минимальный API (и особенно API, позволяющий избежать показа типов, которые не имеют смысла)) и я хотел найти место для оптимизации представления JSON.(Я также просто не хотел, чтобы люди вообще работали с JSON AST, но это была скорее проигрышная битва.) Я все еще думаю, что скрывать конструкторы было правильным решением, хотя я на самом деле не воспользовался этим преимуществом.их отсутствия в оптимизации (пока), и хотя этот вопрос часто возникает.

0 голосов
/ 09 марта 2019

Вы можете использовать is* методы для проверки типа, а затем использовать as*

import io.circe._
import io.circe.parser.parse

object CirceToXml extends App {


  def transformToXMLString(js: Json): String = {
    if (js.isObject) {
      js.asObject.get.toMap.map {
        case (k, v) =>
          s"<$k>${transformToXMLString(v)}</${k}>"
      }.mkString
    } else if (js.isArray) {
      js.asArray.get.map(transformToXMLString).mkString
    } else if (js.isString) {
      js.asString.get
    } else {
      js.toString()
    }
  }

  val json =
    """{
      | "root": {
      |  "sampleboolean": true,
      |  "sampleobj": {
      |    "anInt": 1,
      |    "aString": "string"
      |  },
      |  "objarray": [
      |     {"v1": 1},
      |     {"v2": 2}
      |  ]
      | }
      |}""".stripMargin

  val res = transformToXMLString(parse(json).right.get)
  println(res)
}
...