ExpediaGroup/graphql-kotlin

generator: Couldn't share Union type when includes self-reference type

masa-mfw opened this issue · 5 comments

Issue

Cannot share a union type when a self-reference field is included in the union type definition.

Environment

version: 7.0.2
dependencies: spring boot, gradle

Reproduce

1. Create type classes including self-reference field

data class SimpleTaskType(
    override val id: ID,
    override val name: String,
)

data class TaskGroupType(
    override val id: ID,
    override val name: String,
    val tasks: List<Any>, // Should be list of SimpleTaskType or TaskGroupType
)

2. Configure UnionType

data class TaskGroupType(
    override val id: ID,
    override val name: String,
    @GraphQLUnion(
        name = "Task",
        possibleTypes = [
            SimpleTaskType::class,
            TaskGroupType::class
        ]
    )
    val tasks: List<Any>,
) 

3. Create another type referring to the union type

data class ScheduleType(
    val id: ID,
    @GraphQLUnion(
        name = "Task",
        possibleTypes = [
            SimpleTaskType::class,
            TaskGroupType::class
        ]
    )
    val task: Any,
)

4. Generate SDL and error occurs

error message:

> There was a failure while executing work items
   > A failure occurred while executing com.expediagroup.graphql.plugin.gradle.actions.GenerateSDLAction
      > type Any not found in schema

Work around

Avoid using the same type name.

data class ScheduleType(
    val id: ID,
    @GraphQLUnion(
        name = "UnitedTask", // When "Task" is set, error occurs
        possibleTypes = [
            SimpleTaskType::class,
            TaskGroupType::class
        ]
    )
    val task: Any,
)

However, it generates schema like as:

union Task = SimpleTaskType | TaskGroupType

union UnitedTask = SimpleTaskType | TaskGroupType

type ScheduleType {
  id: ID!
  task: UnitedTask!  # Having different union types for the same entities is not preferable
}

type TaskGroupType implements TaskType {
  id: ID!
  name: String!
  tasks: [Task!]!
}

Question

  • Is this behavior intentional?
  • Are there any solutions to share a union type?

Appendix

Union types can be shared as a single definition when they don't have a self-reference field.
Such as, the following code is perfectly valid.

data class ScheduleType(
    val id: ID,
    @GraphQLUnion(
        name = "UnitedTask", // When "Task" is set, error occurs
        possibleTypes = [
            SimpleTaskType::class,
            TaskGroupType::class
        ]
    )
    val task: Any,
)

data class TodoType(
    val id: ID,
    @GraphQLUnion(
        name = "UnitedTask",
        possibleTypes = [
            SimpleTaskType::class,
            TaskGroupType::class
        ]
    )
    val task: Any, // Share union type as single object
    @GraphQLUnion(
        name = "UnitedTask",
        possibleTypes = [
            SimpleTaskType::class,
            TaskGroupType::class
        ]
    )
    val tasks: List<Any>,  // Share union type as listed objects
    val isDone: Boolean,
)

Sample code

This practice is located at:
https://github.com/masa-mfw/spring-graphql-practice

Hello 👋
Instead of using the @GraphQLUnion which is limited in number of ways, have you tried using marker interfaces or @GraphQLType approach?

@dariuszkuc
Thank you for your reply 😄

I successfully applied @GraphQLType in my sample project.
However, I'm facing issues when applying the same approach to our product code.

What I tried is like following.

data class TaskGroupType(
    override val id: ID,
    override val name: String,
    @GraphQLUnion(
        name = "Task",
        possibleTypes = [
            SimpleTaskType::class,
            TaskGroupType::class
        ]
    )
    val tasks: List<Any>,
) 

data class ScheduleType(
    val id: ID,
    @GraphQLType("Task")
    val task: Any,
)

And error occurs in product code:

type Task not found in schema

I guess this unstable issue might be related to the order in which the schema generator discovers fields.

As for the marker interface, it doesn't seem to meet what I want to do.
From my understanding, this approach requires query arguments to determine the appropriate class response.

I am afraid that I may not fully understand these features.
Would you have any detailed examples for these approach?

Reason why schema generation fails is that @GraphQLType is just a reference to an existing type - it does not generate that type definition. So your Task has to be somehow created.

re: unions through marker interfaces - you simply need to define marker interface and update your types to implement it

interface Task

data class TaskGroupType(
    val id: ID,
    val name: String,
    val tasks: List<Task>,
): Task

data class ScheduleType(
    val id: ID,
    val task: Task,
): Task

Given a simple query then

fun task(): Task { 
  TODO() // your logic goes here
}

Its up to you to determine to return appropriate Task union member.

Thank you!
I agree that these should not be union types, as you pointed out.
This issue arose due to my limited knowledge of GraphQL.
I have learned about GraphQL thanks to this opportunity!

Oops, I misunderstood your suggestion.
Interestingly, when I defined the marker interface, it appeared as a union type in SDL, which was actually my intention.
Thank you for your support!