Создание `Декодера` для произвольного JSON

Я строю конечную точку GraphQL для API, используя Finch, Circe и Sangria. variables в запросе GraphQL это произвольный объект JSON (предположим, что вложенности нет). Так, например, в моем тестовом коде как Strings, вот два примера:

val variables = List(
  "{\n  \"foo\": 123\n}",
  "{\n  \"foo\": \"bar\"\n}"
)

Sangria API ожидает тип для этих Map[String, Any],

Я пробовал несколько способов, но до сих пор не смог написать Decoder для этого в волшебстве. Любая помощь приветствуется.

3 ответа

Sangria API ожидает тип для этих из Map[String, Any]

Это неправда. Переменные для выполнения в сангрии могут быть произвольного типа T, единственное требование, что у вас есть экземпляр InputUnmarshaller[T] тип класс для него. Все библиотеки интеграции маршаллинга предоставляют экземпляр InputUnmarshaller для корреспондента JSON AST тип.

Это означает, что Сангрия-Цирцея определяет InputUnmarshaller[io.circe.Json] и вы можете импортировать его с import sangria.marshalling.circe._,

Вот небольшой и самостоятельный пример того, как вы можете использовать Circe Json в качестве переменных:

import io.circe.Json

import sangria.schema._
import sangria.execution._
import sangria.macros._

import sangria.marshalling.circe._

val query =
  graphql"""
    query ($$foo: Int!, $$bar: Int!) {
      add(a: $$foo, b: $$bar)
    }
  """

val QueryType = ObjectType("Query", fields[Unit, Unit](
  Field("add", IntType,
    arguments = Argument("a", IntType) :: Argument("b", IntType) :: Nil,
    resolve = c ⇒ c.arg[Int]("a") + c.arg[Int]("b"))))

val schema = Schema(QueryType)

val vars = Json.obj(
  "foo" → Json.fromInt(123),
  "bar" → Json.fromInt(456))

val result: Future[Json] =
  Executor.execute(schema, query, variables = vars)

Как вы можете видеть в этом примере, я использовал io.circe.Json в качестве переменных для исполнения. Выполнение даст следующий результат JSON:

{
  "data": {
    "add": 579
  }
}

Вот декодер, который работает.

type GraphQLVariables = Map[String, Any]

val graphQlVariablesDecoder: Decoder[GraphQLVariables] = Decoder.instance { c =>
  val variablesString = c.downField("variables").focus.flatMap(_.asString)
  val parsedVariables = variablesString.flatMap { str =>
    val variablesJsonObject = io.circe.jawn.parse(str).toOption.flatMap(_.asObject)
    variablesJsonObject.map(j => j.toMap.transform { (_, value: Json) =>
      val transformedValue: Any = value.fold(
        (),
        bool => bool,
        number => number.toDouble,
        str => str,
        array => array.map(_.toString),
        obj => obj.toMap.transform((s: String, json: Json) => json.toString)
      )
      transformedValue
    })
  }
  parsedVariables match {
    case None => left(DecodingFailure(s"Unable to decode GraphQL variables", c.history))
    case Some(variables) => right(variables)
  }
}

Мы в основном разбираем JSON, превращаем его в JsonObject, а затем преобразовать значения внутри объекта довольно просто.

Хотя приведенные выше ответы работают для конкретного случая Сангрии, меня интересует оригинальный вопрос: каков наилучший подход в Цирце (который обычно предполагает, что все типы известны заранее) для работы с произвольными кусками Json?

При кодировании / декодировании Json довольно часто указывается 95% Json, но последние 5% - это некоторый тип фрагмента "дополнительных свойств", которым может быть любой объект Json.

Решения, с которыми я играл:

  1. Кодировать / декодировать кусок свободной формы как Map[String,Any], Это означает, что вам придется вводить неявные кодеры / декодеры для Map[String, Any], что может быть сделано, но опасно, так как это неявное может быть перенесено в места, которые вы не собирались.

  2. Кодировать / декодировать кусок свободной формы как Map[String, Json], Это самый простой подход, который поддерживается в Circe из коробки. Но теперь логика сериализации Json просочилась в ваш API (часто вам захочется полностью обернуть материал Json, чтобы позже вы могли поменять его в другие не-json форматы).

  3. Кодировать / декодировать в Stringгде строка должна быть допустимой порцией Json. По крайней мере, вы не заблокировали свой API в определенной библиотеке Json, но не очень приятно просить своих пользователей создавать куски Json таким способом.

  4. Создайте пользовательскую иерархию признаков для хранения данных (например, sealed trait Free; FreeInt(i: Int) extends Free; FreeMap(m: Map[String, Free] extends Free; ...). Теперь вы можете создавать специальные кодеры / декодеры для него. Но то, что вы действительно сделали, это скопировали иерархию типов Json, которая уже существует в Circe.

Я больше склоняюсь к варианту 3. Так как он наиболее гибкий и вводит наименьшее количество зависимостей в API. Но ни один из них не является полностью удовлетворительным. Есть другие идеи?

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