sangria-graphql / sangria

Scala GraphQL implementation
https://sangria-graphql.github.io
Apache License 2.0
1.96k stars 226 forks source link

How to provide nested list of case class as an argument to the GQL query #715

Open er-rishi opened 3 years ago

er-rishi commented 3 years ago

One of the input of the GQL query is the nested list of case class. For ex: case class User (id: Int, name:String)

val user: InputObjectType[User] = deriveInputObjectType[User]
val arg = Argument("users", OptionInputType(ListInputType(ListInputType(user))))

when I run the application I am getting the following error:

java.lang.ClassCastException: spray.json.JsArray cannot be cast to scala.collection.Seq
    at sangria.marshalling.FromInput$SeqFromInput.fromResult(FromInput.scala:23)
    at sangria.marshalling.FromInput$SeqFromInput.fromResult(FromInput.scala:19)
    at sangria.marshalling.FromInput$SeqFromInput.$anonfun$fromResult$1(FromInput.scala:29)
    at scala.collection.TraversableLike.$anonfun$map$1(TraversableLike.scala:237)
    at scala.collection.Iterator.foreach(Iterator.scala:941)
    at scala.collection.Iterator.foreach$(Iterator.scala:941)
    at scala.collection.AbstractIterator.foreach(Iterator.scala:1429)
    at scala.collection.IterableLike.foreach(IterableLike.scala:74)
    at scala.collection.IterableLike.foreach$(IterableLike.scala:73)
    at scala.collection.AbstractIterable.foreach(Iterable.scala:56)
    at scala.collection.TraversableLike.map(TraversableLike.scala:237)
    at scala.collection.TraversableLike.map$(TraversableLike.scala:230)
    at scala.collection.AbstractTraversable.map(Traversable.scala:108)
    at sangria.marshalling.FromInput$SeqFromInput.fromResult(FromInput.scala:25)
    at sangria.marshalling.FromInput$SeqFromInput.fromResult(FromInput.scala:19)
    at sangria.execution.ValueCollector$.$anonfun$getArgumentValues$6(ValueCollector.scala:149)
    at sangria.execution.ValueCoercionHelper.resolveMapValue(ValueCoercionHelper.scala:162)
    at sangria.execution.ValueCollector$.$anonfun$getArgumentValues$4(ValueCollector.scala:156)
    at scala.collection.LinearSeqOptimized.foldLeft(LinearSeqOptimized.scala:126)
    at scala.collection.LinearSeqOptimized.foldLeft$(LinearSeqOptimized.scala:122)
    at scala.collection.immutable.List.foldLeft(List.scala:89)
    at sangria.execution.ValueCollector$.getArgumentValues(ValueCollector.scala:132)
    at sangria.execution.ValueCollector.getArgumentValues(ValueCollector.scala:103)
    at sangria.execution.ValueCollector.$anonfun$getFieldArgumentValues$1(ValueCollector.scala:87)
    at sangria.util.ConcurrentHashMapCache.getOrElseUpdate(ConcurrentHashMapCache.scala:29)
    at sangria.execution.ValueCollector.getFieldArgumentValues(ValueCollector.scala:87)
    at sangria.execution.Resolver.resolveField(Resolver.scala:1412)
    at sangria.execution.Resolver.$anonfun$collectActionsPar$1(Resolver.scala:709)
    at scala.collection.TraversableOnce.$anonfun$foldLeft$1(TraversableOnce.scala:160)
    at scala.collection.TraversableOnce.$anonfun$foldLeft$1$adapted(TraversableOnce.scala:160)
    at scala.collection.Iterator.foreach(Iterator.scala:941)
    at scala.collection.Iterator.foreach$(Iterator.scala:941)
    at scala.collection.AbstractIterator.foreach(Iterator.scala:1429)
    at scala.collection.IterableLike.foreach(IterableLike.scala:74)
    at scala.collection.IterableLike.foreach$(IterableLike.scala:73)
    at scala.collection.AbstractIterable.foreach(Iterable.scala:56)
    at scala.collection.TraversableOnce.foldLeft(TraversableOnce.scala:160)
    at scala.collection.TraversableOnce.foldLeft$(TraversableOnce.scala:158)
    at scala.collection.AbstractTraversable.foldLeft(Traversable.scala:108)
    at sangria.execution.Resolver.collectActionsPar(Resolver.scala:699)
    at sangria.execution.Resolver.resolveFieldsPar(Resolver.scala:45)
    at sangria.execution.Executor.executeOperation(Executor.scala:275)
    at sangria.execution.Executor.$anonfun$execute$7(Executor.scala:206)
    at scala.concurrent.Future.$anonfun$flatMap$1(Future.scala:307)
    at scala.concurrent.impl.Promise.$anonfun$transformWith$1(Promise.scala:41)
    at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:64)
    at akka.dispatch.BatchingExecutor$AbstractBatch.processBatch(BatchingExecutor.scala:55)
    at akka.dispatch.BatchingExecutor$BlockableBatch.$anonfun$run$1(BatchingExecutor.scala:92)
    at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23)
    at scala.concurrent.BlockContext$.withBlockContext(BlockContext.scala:85)
    at akka.dispatch.BatchingExecutor$BlockableBatch.run(BatchingExecutor.scala:92)
    at akka.dispatch.TaskInvocation.run(AbstractDispatcher.scala:41)
    at akka.dispatch.ForkJoinExecutorConfigurator$AkkaForkJoinTask.exec(ForkJoinExecutorConfigurator.scala:49)
    at akka.dispatch.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
    at akka.dispatch.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
    at akka.dispatch.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
    at akka.dispatch.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)

input request:

{
  "users": [
    [
      {
        "id": "8",
        "name": "BEGINS_WITH"
      },
      {
        "id": "8",
        "name": "BEGINS_WITH"
      }
    ],
    [
      {
        "id": "8",
        "name": "BEGINS_WITH"
      }
    ]
  ]
}

Any help will be appriciated. Thanks

liam923 commented 2 years ago

I've been running into the same issue and believe that it's a bug. Did you find a way around this?

yanns commented 2 years ago

If you are using spray-json, you should use https://github.com/sangria-graphql/sangria-spray-json/blob/main/src/main/scala/sangria/marshalling/sprayJson.scala somewhere. It's strange that it does not appear in the stack trace.

liam923 commented 2 years ago

@yanns I've looked into the issue more, and I'm convinced it is a bug with sangria. I created a test case for it on my own fork, located here. I should note that although this test and the above issue use spray-json, I have been running into the same issue with circe.

The issue seems to stem from a lack of type safety within the default FromInput implementations. The cast located here in SeqFromInput is where that error is occurring. With a array of depth 1 ([SomeInputObject!]! for example), this works fine, because the node it is given is actually a Vector. However, when it recursively calls itself, which it does when the depth is greater than 1 ([[SomeInputObject!]!]! for example), node is instead a representation of an array in whatever json library is being used. For spray-json, this is JsArray, which explains the java.lang.ClassCastException: spray.json.JsArray cannot be cast to scala.collection.Seq error. For circe, this is a Json.JArray.

I have been able to circumvent this issue by creating my own implementation of SeqFromInput, as well as optionInput. Those are below:

case class SeqInput[T](delegate: FromInput[T]) extends FromInput.SeqFromInput[T](delegate) {
  override def fromResult(node: marshaller.Node): Seq[T] = {
    super.fromResult(nodeToVector(node).asInstanceOf[marshaller.Node])
  }

  private def nodeToVector(node: marshaller.Node): Vector[_] =
    node match {
      case json: Json =>
        json.asArray
          // the get or else will fail, but there's no way to recover
          .getOrElse(json.asInstanceOf[Vector[_]])
      case _ => node.asInstanceOf[Vector[_]]
    }
}

case class OptInput[T](delegate: FromInput[T]) extends FromInput[Option[T]] {
  override val marshaller: ResultMarshaller = delegate.marshaller

  override def fromResult(node: marshaller.Node): Option[T] = {
    node match {
      case opt: Option[_] => opt.map(elem => delegate.fromResult(elem.asInstanceOf[delegate.marshaller.Node]))
      case elem           => Some(delegate.fromResult(elem.asInstanceOf[delegate.marshaller.Node]))
    }
  }
}

Clearly, these are circe specific and do not work in the general case. However, they help demonstrate what the issue is and function as a stopgap solution for now. Unfortunately, I am not too familiar with the sangria source code, so I'm not sure if I'd be able to fix this myself. It seems to me that a solution might involve having FromInput be given a reference to an InputUnmarshaller and then doing something similar to above.

yanns commented 2 years ago

Thanks for the detailed information. Yes, it seems you found something..

How to tackle this:

If someone wants to try this, please comment.

muuki88 commented 1 year ago

Related to #449