Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GraphQL schema generation fails with sealed interfaces #2237

Open
faskan opened this issue Dec 6, 2024 · 3 comments
Open

GraphQL schema generation fails with sealed interfaces #2237

faskan opened this issue Dec 6, 2024 · 3 comments

Comments

@faskan
Copy link

faskan commented Dec 6, 2024

Quarkus startup and GraphQL schema generation fails with sealed interfaces.
Here is my reproducer.

@GraphQLApi
public class HelloGraphQLResource {

    @Query
    public List<CustomerProduct> getProducts() {

        return List.of(new InternetLine("Test", 1000), new MobileLine("Test", "0470000000"));
    }
}

sealed interface CustomerProduct permits InternetLine, MobileLine {
    String name();
}

record InternetLine(String name, int speed) implements CustomerProduct {

}

record MobileLine(String name, String mobileNumber) implements CustomerProduct {

}

Full source code in github - https://github.com/faskan/graphql

Running this code results in "type not found in schema" exception.
Stacktrace:

graphql.AssertException: type CustomerProduct not found in schema
	at graphql.Assert.throwAssert(Assert.java:207)
	at graphql.Assert.assertNotNull(Assert.java:45)
	at graphql.schema.GraphQLTypeResolvingVisitor.handleTypeReference(GraphQLTypeResolvingVisitor.java:49)
	at graphql.schema.GraphQLTypeResolvingVisitor.visitGraphQLTypeReference(GraphQLTypeResolvingVisitor.java:44)
	at graphql.schema.GraphQLTypeReference.accept(GraphQLTypeReference.java:62)
	at graphql.schema.SchemaTraverser$TraverserDelegateVisitor.enter(SchemaTraverser.java:111)
	at graphql.util.Traverser.traverse(Traverser.java:144)
	at graphql.schema.SchemaTraverser.doTraverse(SchemaTraverser.java:98)
	at graphql.schema.SchemaTraverser.depthFirst(SchemaTraverser.java:88)
	at graphql.schema.SchemaTraverser.depthFirst(SchemaTraverser.java:81)
	at graphql.schema.impl.SchemaUtil.replaceTypeReferences(SchemaUtil.java:105)
	at graphql.schema.GraphQLSchema$Builder.buildImpl(GraphQLSchema.java:878)
	at graphql.schema.GraphQLSchema$Builder.build(GraphQLSchema.java:841)
	at io.smallrye.graphql.bootstrap.Bootstrap.generateGraphQLSchema(Bootstrap.java:229)
	at io.smallrye.graphql.bootstrap.Bootstrap.bootstrap(Bootstrap.java:129)
	at io.smallrye.graphql.cdi.producer.GraphQLProducer.initialize(GraphQLProducer.java:59)
	at io.smallrye.graphql.cdi.producer.GraphQLProducer.initialize(GraphQLProducer.java:49)
	at io.smallrye.graphql.cdi.producer.GraphQLProducer.initialize(GraphQLProducer.java:39)
	at io.smallrye.graphql.cdi.producer.GraphQLProducer.initialize(GraphQLProducer.java:34)
	at io.smallrye.graphql.cdi.producer.GraphQLProducer_ClientProxy.initialize(Unknown Source)
	at io.quarkus.smallrye.graphql.runtime.SmallRyeGraphQLRecorder.createExecutionService(SmallRyeGraphQLRecorder.java:46)
	at io.quarkus.deployment.steps.SmallRyeGraphQLProcessor$buildExecutionService450434440.deploy_1(Unknown Source)
	at io.quarkus.deployment.steps.SmallRyeGraphQLProcessor$buildExecutionService450434440.deploy(Unknown Source)
	at io.quarkus.runner.ApplicationImpl.<clinit>(Unknown Source)
	at java.base/jdk.internal.misc.Unsafe.ensureClassInitialized0(Native Method)
	at java.base/jdk.internal.misc.Unsafe.ensureClassInitialized(Unsafe.java:1160)
	at java.base/jdk.internal.reflect.MethodHandleAccessorFactory.ensureClassInitialized(MethodHandleAccessorFactory.java:300)
	at java.base/jdk.internal.reflect.MethodHandleAccessorFactory.newConstructorAccessor(MethodHandleAccessorFactory.java:103)
	at java.base/jdk.internal.reflect.ReflectionFactory.newConstructorAccessor(ReflectionFactory.java:200)
	at java.base/java.lang.reflect.Constructor.acquireConstructorAccessor(Constructor.java:549)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:486)
	at io.quarkus.runtime.Quarkus.run(Quarkus.java:70)
	at io.quarkus.runtime.Quarkus.run(Quarkus.java:44)
	at io.quarkus.runtime.Quarkus.run(Quarkus.java:124)
	at io.quarkus.runner.GeneratedMain.main(Unknown Source)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at io.quarkus.runner.bootstrap.StartupActionImpl$1.run(StartupActionImpl.java:116)
	at java.base/java.lang.Thread.run(Thread.java:1583)
Resulted in: io.quarkus.dev.appstate.ApplicationStartException: graphql.AssertException: type CustomerProduct not found in schema
	at io.quarkus.dev.appstate.ApplicationStateNotification.waitForApplicationStart(ApplicationStateNotification.java:63)
	at io.quarkus.runner.bootstrap.StartupActionImpl.runMainClass(StartupActionImpl.java:142)
	at io.quarkus.deployment.dev.IsolatedDevModeMain.restartApp(IsolatedDevModeMain.java:202)
	at io.quarkus.deployment.dev.IsolatedDevModeMain.restartCallback(IsolatedDevModeMain.java:183)
	at io.quarkus.deployment.dev.RuntimeUpdatesProcessor.doScan(RuntimeUpdatesProcessor.java:555)
	at io.quarkus.deployment.dev.RuntimeUpdatesProcessor.doScan(RuntimeUpdatesProcessor.java:455)
	at io.quarkus.vertx.http.runtime.devmode.VertxHttpHotReplacementSetup$6.call(VertxHttpHotReplacementSetup.java:161)
	at io.quarkus.vertx.http.runtime.devmode.VertxHttpHotReplacementSetup$6.call(VertxHttpHotReplacementSetup.java:148)
	at io.vertx.core.impl.ContextImpl.lambda$executeBlocking$4(ContextImpl.java:192)
	at io.vertx.core.impl.ContextInternal.dispatch(ContextInternal.java:270)
	at io.vertx.core.impl.ContextImpl$1.execute(ContextImpl.java:221)
	at io.vertx.core.impl.WorkerTask.run(WorkerTask.java:56)
	at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18)
	at org.jboss.threads.EnhancedQueueExecutor$Task.doRunWith(EnhancedQueueExecutor.java:2675)
	at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2654)
	at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1591)
	at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:11)
	at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:11)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	... 1 more
Resulted in: io.vertx.core.impl.NoStackTraceException

I saw similar issue reported in quarkusio/quarkus#12846 but seemed like it was raised for kotlin and for kotlin it works well with sealed interfaces and data classes.

@mskacelik
Copy link
Contributor

I do not think that we support polymorphism (maybe someone can double check me) of this kind, instead what we use is Union, see: https://quarkus.io/guides/smallrye-graphql#unions

So, if you just annotate the interface like this:

    // the rest of the code ...
    @Union
    sealed interface CustomerProduct permits InternetLine, MobileLine {
        String name();
    }
    // the rest of the code ...

It will create a GraphQL schema like this:

union CustomerProduct = InternetLine | MobileLine

type InternetLine {
  name: String
  speed: Int!
}

type MobileLine {
  mobileNumber: String
  name: String
}

"Query root"
type Query {
  products: [CustomerProduct]
}

And GraphQL request can look like this (by enumerating all the children):

query {
  products {
    ... on MobileLine {
      name
      # mobileNumber
    }
    ... on InternetLine {
      name
      # speed
    }
  }
}

Response:

{
  "data": {
    "products": [
      {
        "name": "Test"
      },
      {
        "name": "Test"
      }
    ]
  }
}

@faskan
Copy link
Author

faskan commented Dec 6, 2024

@mskacelik but it works in kotlin without the union

@GraphQLApi
class HelloGraphQLResource {

    @Query("customerProducts")
    @Description("return all customer products")
    fun sayHello(name: String): CustomerProducts {
        return CustomerProducts(
            mobileLines = listOf(
                CustomerProduct.MobileLine("1", "12345678")
            ),
            internetLine = listOf(
                CustomerProduct.InternetLine("3", 100)
            )
        )
    }

}

data class CustomerProducts(
    val mobileLines: List<CustomerProduct>,
    val internetLine: List<CustomerProduct>
)
sealed interface CustomerProduct {
    val id: String

    data class MobileLine(override val id: String, val phoneNumber: String) : CustomerProduct
    data class InternetLine(override val id: String, val speed: Int) : CustomerProduct

}

creates a schema

interface CustomerProduct {
  id: String!
}

type CustomerProducts {
  internetLine: [CustomerProduct]!
  mobileLines: [CustomerProduct]!
}

type InternetLine implements CustomerProduct {
  id: String!
  speed: Int!
}

type MobileLine implements CustomerProduct {
  id: String!
  phoneNumber: String!
}

"Query root"
type Query {
  "return all customer products"
  customerProducts(name: String!): CustomerProducts!
}

@mskacelik
Copy link
Contributor

mskacelik commented Dec 9, 2024

@faskan Oh, now I understand. The problem was this line:


We only accepted output property methods getXXX for the interfaces, which is a problem with records since it does not follow the getXXX/setXXX, unlike classes.

I made a PR with a possible solution of allowing the creation of fields for the interfaces with the @org.eclipse.microprofile.graphql.Name` annotation:

interface Interface {
    @Name("name") // @Name("") also works
    String name();
}

PR: #2238

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants