Создание `Декодера` для произвольного JSON
Я строю конечную точку GraphQL для API, используя Finch, Circe и Sangria. variables
в запросе GraphQL это произвольный объект JSON (предположим, что вложенности нет). Так, например, в моем тестовом коде как String
s, вот два примера:
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.
Решения, с которыми я играл:
Кодировать / декодировать кусок свободной формы как
Map[String,Any]
, Это означает, что вам придется вводить неявные кодеры / декодеры дляMap[String, Any]
, что может быть сделано, но опасно, так как это неявное может быть перенесено в места, которые вы не собирались.Кодировать / декодировать кусок свободной формы как
Map[String, Json]
, Это самый простой подход, который поддерживается в Circe из коробки. Но теперь логика сериализации Json просочилась в ваш API (часто вам захочется полностью обернуть материал Json, чтобы позже вы могли поменять его в другие не-json форматы).Кодировать / декодировать в
String
где строка должна быть допустимой порцией Json. По крайней мере, вы не заблокировали свой API в определенной библиотеке Json, но не очень приятно просить своих пользователей создавать куски Json таким способом.Создайте пользовательскую иерархию признаков для хранения данных (например,
sealed trait Free; FreeInt(i: Int) extends Free; FreeMap(m: Map[String, Free] extends Free; ...
). Теперь вы можете создавать специальные кодеры / декодеры для него. Но то, что вы действительно сделали, это скопировали иерархию типов Json, которая уже существует в Circe.
Я больше склоняюсь к варианту 3. Так как он наиболее гибкий и вводит наименьшее количество зависимостей в API. Но ни один из них не является полностью удовлетворительным. Есть другие идеи?