Tom Adams
Tom Adams

Reputation: 143

Creating a `Decoder` for arbitrary JSON

I am building a GraphQL endpoint for an API using Finch, Circe and Sangria. The variables that come through in a GraphQL query are basically an arbitrary JSON object (let's assume there's no nesting). So for example, in my test code as Strings, here are two examples:

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

The Sangria API expects a type for these of Map[String, Any].

I've tried a bunch of ways but have so far been unable to write a Decoder for this in Circe. Any help appreciated.

Upvotes: 3

Views: 1442

Answers (3)

Aish
Aish

Reputation: 97

Although the above answers work for the specific case of Sangria, I'm interested in the original question: What's the best approach in Circe (which generally assumes that all types are known up front) for dealing with arbitrary chunks of Json?

It's fairly common when encoding/decoding Json that 95% of the Json is specified, but the last 5% is some type of "additional properties" chunk which can be any Json object.

Solutions I've played with:

  1. Encode/Decode the free-form chunk as Map[String,Any]. This means you'll have to introduce implicit encoders/decoders for Map[String, Any], which can be done, but is dangerous as that implicit can be pulled into places you didn't intend.

  2. Encode/Decode the free-form chunk as Map[String, Json]. This is the easiest approach and is supported out of the box in Circe. But now the Json serialization logic has leaked out into your API (often you'll want to keep the Json stuff completely wrapped, so you can swap in other non-json formats later).

  3. Encode/Decode to a String, where the string is required to be a valid Json chunk. At least you haven't locked your API into a specific Json library, but it doesn't feel very nice to have to ask your users to create Json chunks in this manual way.

  4. Create a custom trait hierarchy to hold the data (e.g sealed trait Free; FreeInt(i: Int) extends Free; FreeMap(m: Map[String, Free] extends Free; ...). Now you you can create specific encoders/decoders for it. But what you've really done is replicate the Json type hierarchy that already exists in Circe.

I'm leaning more toward option 3. Since it's the most flexible, and will introduce the least dependencies in the API. But none of them are entirely satisfying. Any other ideas?

Upvotes: 0

tenshi
tenshi

Reputation: 26576

The Sangria API expects a type for these of Map[String, Any]

This is not true. Variables for an execution in sangria can be of an arbitrary type T, the only requirement that you have an instance of InputUnmarshaller[T] type class for it. All marshalling integration libraries provide an instance of InputUnmarshaller for correspondent JSON AST type.

This means that sangria-circe defines InputUnmarshaller[io.circe.Json] and you can import it with import sangria.marshalling.circe._.

Here is a small and self-contained example of how you can use circe Json as a variables:

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)

As you can see in this example, I used io.circe.Json as variables for an execution. The execution would produce following result JSON:

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

Upvotes: 6

Tom Adams
Tom Adams

Reputation: 143

Here's a decoder that works.

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)
  }
}

We basically parse the JSON, turn it into a JsonObject, then transform the values within the object fairly simplistically.

Upvotes: 1

Related Questions