ListType invalid for dynamically created Query OutputType

385 views
Skip to first unread message

Peter Hunsberger

unread,
Mar 15, 2017, 1:48:13 PM3/15/17
to sangria-graphql
I went to add a query handler for my dynamically generated schema root that would allow an "all" type query and am running up against some restrictions on what OutputTypes can be used.  If I define the query Field as:

     Field(label, OptionType(qtype),....

It works fine, but I get a single result back from the query.  If instead I define the query field as:

    Field(label, OptionType(ListType(qtype)),...

Or

    Field(label, ListType(qtype),...

I run up against an implicitNotFound message from line 337 of Schema.scala in Sangria

Think there needs to be another implicit ValidOutType added here to allow a Seq[Any] and maybe one for Option[Seq[Any]]?  Or am I missing something obvious?

Peter Hunsberger

unread,
Mar 16, 2017, 1:01:54 PM3/16/17
to sangria-graphql
Took a look at implementing this and quickly found the reason it's not currently implemented:  Scala can't distinguish between Option[Any] and Seq[Any] as targets for the implicit.  Not sure what the solution is, for my case I could build a version of Sangria that just removes the compile time type checking here but that won't help anyone else who wants to  support List forms of root query with a dynamic schema...   Maybe Sangria could add some form of traits over the Output type so that one could do something like OutputTypeOption[String], OutputTypeList[String] at the time one is defining the schema?

Oleg Ilyenko

unread,
Mar 16, 2017, 1:24:04 PM3/16/17
to sangria-graphql
I think the value/action that is returned from a resolve function has a wrong type (the type of returned value contradicts the field's type). Here is an example of more complex type from an example application:


As you can see, it's even more complex than in your example: `OptionType(ListType(OptionType(EpisodeEnum)))` and it compiles just fine. 

I would be helpful if you could share the full compiler error message and maybe make a small, self-contained example that reproduces your issue.

Peter Hunsberger

unread,
Mar 16, 2017, 3:31:15 PM3/16/17
to sangria-graphql
The Field with the resolve is built as:

  Field( label, OptionType(qtype), arguments = ...,
              resolve = ctx => ctx.ctx.queryGraph(propName, ctx), ... )

where qtype is based on the metadata I get back from DSE graph.  As such, this comes from a function that has to be generic over all the possible types and thus (currently) has a return type of ObjectType[Any, Any] (but will be a specific OutputType at run time).

queryGraph has a return type of Option[GraphNode]

This all works fine.  I now want to add in handling for a root query that could return more than one result.  In this case I've attempted to define several variations on the Field, for example:

  Field( allLabel, ListType(qtype), arguments = List(), 
              resolve = ctx => ctx.ctx.queryAllGraph(propName, ctx), ....)

where queryAllGraph (in this case) has a return type of Seq[GraphNode]

This results in the compile time error I referred to earlier:

[error] ...\sangriaDseGraphIntegration\SchemaBuilder.scala:145: Any is invalid type for the resulting GraphQL type Seq[Any].
[error]             Field(allLabel, ListType(qtype),
[error]                     ^
[error] one error found

Again, I don't know the specific Sangria type at compile time as it will be determined at run time from the DSE metadata, but perhaps there is something more specific than Any that I can use that will somehow satisfy the type checking?

Oleg Ilyenko

unread,
Mar 16, 2017, 3:47:13 PM3/16/17
to sangria-graphql
Since it's so dynamic by nature, I guess you need to cast... e.g. `ctx.ctx.queryAllGraph(propName, ctx), ....).asInstanceOf[Seq[Any]]`

Peter Hunsberger

unread,
Mar 16, 2017, 4:48:54 PM3/16/17
to sangria-graphql
Afraid the type check in Schema still kicks in.  It is coded up as:

@implicitNotFound(msg = "${Res} is invalid type for the resulting GraphQL type ${Out}.")
trait ValidOutType[-Res, +Out]

object ValidOutType {
  private val valid = new ValidOutType[Any, Any] {}

  implicit def validSubclass[Res, Out](implicit ev: Res <:< Out) = valid.asInstanceOf[ValidOutType[Res, Out]]
  implicit def validNothing[Out] = valid.asInstanceOf[ValidOutType[Nothing, Out]]
  implicit def validOption[Res, Out](implicit ev: Res <:< Out) = valid.asInstanceOf[ValidOutType[Res, Option[Out]]]
}

I tried adding in:

  implicit def validSeq[Res, Out](implicit ev: Res <:< Out) = valid.asInstanceOf[ValidOutType[Res, Seq[Out]]]

But that does not compile. I'm a little lost when it comes to the contravariant / covariant type signature here but I think Option[Out] and Seq[Out] are ambiguous over Any here?

Peter Hunsberger

unread,
Mar 20, 2017, 9:58:52 AM3/20/17
to sangria-graphql
Any thoughts on this Oleg?  I'm thinking maybe I can create some kind of adapter OutputType on the Sangria side but if I start on that want to make sure any solution would be something that would be compatible with any direction you think this should go?

Oleg Ilyenko

unread,
Mar 20, 2017, 10:57:14 AM3/20/17
to sangria-graphql
I don't think that `ValidOutType` is an issue here. For example, the new instance you tried to define:

implicit def validSeq[Res, Out](implicit ev: Res <:< Out) = valid.asInstanceOf[ValidOutType[Res, Seq[Out]]]

Generally allows to return any type from a field that is defined for a `Seq` of this type. In most scenarios, it will result in class cast exception during the execution.

Have you already tried to do the cast in the resolve function where you know that you expect a sequence and nor a single element?

  Field(allLabel, ListType(qtype), arguments = List(), 
    resolve = ctx => ctx.ctx.queryAllGraph(propName, ctx).asInstanceOf[Seq[Any]])

If `queryAllGraph` always returns a single element (you know that it is not a `Seq`), then maybe you can try something like this:

  Field(allLabel, ListType(qtype), arguments = List(), 
    resolve = ctx => Seq(ctx.ctx.queryAllGraph(propName, ctx)))

Generally, the compiler complains because, based on your field definition, field's resolve function supposed to return `Seq[Any]`, but it returns `Any`. So `ValidOutType` as intended, and the simple way to solve it would be to either cast result value to `Seq[Any]` (if you sure that always would be a `Seq`) or make put it into a `Seq` as I shown above.

Peter Hunsberger

unread,
Mar 20, 2017, 12:14:02 PM3/20/17
to sangria-graphql
The problem is at compile time, not at run time, so a cast will not help.

Oleg Ilyenko

unread,
Mar 20, 2017, 12:21:57 PM3/20/17
to sangria-graphql
> The problem is at compile time, not at run time, so a cast will not help.

It will help because casting a value changes its type seen by the compiler. Here is a small example:

val list: AnyRef = Seq("foo", "bar")

// will not compile
val list2
: Seq[String] = list

// compiles successfully
val list3: Seq[String] = list.asIntanceOf[Seq[String]]

It would be great if you could isolate this issue in a minimalistic and self-contained example that reproduces it so that I can look at it in more detail and maybe provide a better suggestion.

Peter Hunsberger

unread,
Mar 20, 2017, 12:50:32 PM3/20/17
to sangria-graphql
As I previously posted adding the cast does not help! 

The problem is that Seq[Any] and Option[Any] are ambiguous when it comes to resolving the the implicit type check at compile time or when trying to even create a new implicit type check.  If the resolve function does not have to actually be typed with Any then you are ok since that can be checked against the implicit's but as the Sangria code base currently sits it is impossible to have a resolve function that has multiple possible return types for Seq, the implicit check for Option will conflict.

 As I previously posted, the code in question is as follows:

  I want to add in handling for a root query that could return more than one result.  In this case I've attempted to define several variations on the Field, for example:

  Field( allLabel, ListType(qtype), arguments = List(), 
              resolve = ctx => ctx.ctx.queryAllGraph(propName, ctx), ....)

where queryAllGraph (in this case) has a return type of Seq[GraphNode] (and qtype will be a Sangrai Output type picked at run time).

This results in the compile time error I referred to earlier:

[error] ...\sangriaDseGraphIntegration\SchemaBuilder.scala:145: Any is invalid type for the resulting GraphQL type Seq[Any].
[error]             Field(allLabel, ListType(qtype),
[error]                     ^
[error] one error found

Again, I don't know the specific Sangria type at compile time as it will be determined at run time from the DSE metadata.  What I need is something more specific than Any that I can use that will somehow satisfy the type checking...


Oleg Ilyenko

unread,
Mar 20, 2017, 1:06:48 PM3/20/17
to sangria-graphql
Thanks for a more detailed explanation. I believe that at this point a small self-contain example of the issue would help a lot. I probably misunderstood something or maybe some context information is still missing. A minimal executable example can clarify the issue and help me to understand it and show the full the context around it.

Peter Hunsberger

unread,
Mar 20, 2017, 1:58:51 PM3/20/17
to sangria-graphql
I think this really is as simple as the few lines of code I've shown. You should be able to reproduce this by trying to create a query type with a field like:

Field(allLabel, ListType(qtype), arguments = List(), resolve = ctx => ctx.ctx.queryAllGraph(propName, ctx), ....)

Where queryAllGraph() has a return type of Seq[Vertex] (in my case) and qtype has a type of OutputType[Any]

In my case Vertex is from DataStax Enterprise Graph, but can I think it can be any class that will have the output results?  From what I can tell it's the fact that it is contained within a Seq that is the problem.

It seems that the issue is that since OutputType is over Any, the implicit validity checks can only check Seq[T] against Option[T] and this won't work.

As I said previously, you can see the base problem by going into Schema.scala and trying to add the validity check:

  implicit def validSeq[Res, Out](implicit ev: Res <:< Out) = valid.asInstanceOf[ValidOutType[Res, Seq[Out]]]

around line 360.  This cannot compile with the current 

  implicit def validOption[Res, Out](implicit ev: Res <:< Out) = valid.asInstanceOf[ValidOutType[Res, Option[Out]]]

definition in place since the two definitions then overlap each other even as implicit's before it comes to trying and match up against the code above...

There either needs to be some return type more specific than Any that I can use to match the  implicit (so I don't need the new validity check) or there needs to be some kind of wrapper validity check that can be used to handle both Seq[Any] and Option[Any].  I'm playing with the latter at the moment but higher order types in Scala are new to me and I'm struggling to do this without having to add in run time type checking.

Oleg Ilyenko

unread,
Mar 20, 2017, 3:11:51 PM3/20/17
to sangria-graphql
I tried to follow your description and created this example:

case class Vertex(id: String)

def queryAllGraph(): Seq[Vertex] = Seq(Vertex("1"), Vertex("2"))

val qtype: OutputType[Any] = ObjectType("Vertex", fields[Unit, Vertex](
  Field("id", StringType, resolve = _.value.id)))

val QueryType = ObjectType("Query", fields[Unit, Unit](
  Field("vertices", ListType(qtype), resolve = _ ⇒ queryAllGraph())))

val testSchema = Schema(QueryType)

println(Executor.execute(testSchema, graphql"{vertices {id}}").await.prettyPrint)

As you see, `queryAllGraph` is of type `Seq[Vertex]` and `qtype` is of type `OutputType[Any]`. It compiles and runs just fine. The output is:

{
  "data": {
    "vertices": [{
      "id": "1"
    }, {
      "id": "2"
    }]
  }
}

Have I missed something?

Peter Hunsberger

unread,
Mar 21, 2017, 9:48:25 AM3/21/17
to sangria-graphql
I'll see if I can build this out and double check, but from visual inspection the main difference is that I have to push context and  multiple data types into the fields and thus my Fields have type [Any, Any] and not [Unit. Unit] or [Unit, Vertex]. 

Peter Hunsberger

unread,
Mar 21, 2017, 11:29:00 AM3/21/17
to sangria-graphql
I'm failing to figure out what you intended with "graphql" in your execute:

println(Executor.execute(testSchema, graphql"{vertices {id}}").await.prettyPrint)

???

Oleg Ilyenko

unread,
Mar 21, 2017, 3:22:09 PM3/21/17
to sangria-graphql
I would suggest you to check out Getting Started docs. This particular section describes `graphql` macro:

Peter Hunsberger

unread,
Mar 21, 2017, 5:23:25 PM3/21/17
to sangria-graphql
Ah, I had seen the macro usage a long time ago but hadn't used them since everything I'm doing is dynamically constructed.

Ended up just using the QueryParser and I've now found my problem.  The original code is rather complicated but ultimately there is an error on my side caused by having some debug code in the middle of the resolve call which caused resolve to return both the expected result or Unit.  The misleading issue is that this works fine for the case where you are returning Option[OutputType[Any]] but fails for the case where one returns Seq[OutputType[Any]] Ultimately, the reason it worked at all is because of the implicit for ValidOption

Not sure if there is any way to make this kind of error easier to spot.  After a bunch of experimentation I've reduced the problem to the rather artificial code snippets that follow:

        def buildSchema: Schema[Any, Any] = {
Schema( ObjectType("Query", queryFields()) )
}
def queryFields(): List[Field[Any,Any]] = { 
val flds =  new ListBuffer[Field[Any, Any]]
flds.append(Field("vertex",OptionType(qtype), arguments = List( Argument( "id", StringType, "id")), resolve = ctx => ctx.ctx match {
case r: Repo => r.queryGraph()                                             // returns Option[Vertex] 
case other => println( "Context for queryFields is " + other )    // match is not returning Any
}))
flds.append(Field("vertices", ListType(qtype), resolve = ctx => ctx.ctx match {
case r: Repo => r.queryAllGraph()                                         // returns Seq[Vertex]
case other => println( "Context for queryFields is " + other )    // match is now returning Any
}))
flds.toList
}


I'm adding in the Repo at execute time as the user context:

   Executor.execute(buildSchema, query, new Repo())

The first Field definition compiles fine, the second one will not find a matching implicit.  Really, you don't want the first case to work either.  Not sure what to do about that?
Reply all
Reply to author
Forward
0 new messages