Skip to content

Empty union base definitions rejected before extensions are merged (spec violation) #4200

@rstata

Description

@rstata

Describe the bug

The spec grammar:

(UnionTypeDefinition : Description? union Name Directives[Const]? UnionMemberTypes?)

makes UnionMemberTypes optional. SchemaParser correctly parses this SDL and TypeDefinitionRegistry correctly shows the union extensions. However, SchemaGenerator.makeExecutableSchema() fails validation with:

SchemaProblem{errors=[Union type 'Pet' must include one or more member types.]}

Root cause is that the validation checks the base definition before merging extension members (which isn't done for other types like interfaces). For deeper dive on root cause see this link.

(GraphQL Java 25.0)

To Reproduce

import graphql.schema.idl.SchemaParser
import graphql.schema.idl.UnExecutableSchemaGenerator
import org.junit.jupiter.api.Test

/**
 * Demonstrates a graphql-java bug: empty union base definitions with members
 * added via extensions are rejected, even though the GraphQL spec allows this.
 *
 * The spec grammar (UnionTypeDefinition : Description? union Name Directives[Const]? UnionMemberTypes?)
 * makes UnionMemberTypes optional. SchemaParser correctly parses this SDL and
 * TypeDefinitionRegistry correctly shows the union extensions. However,
 * SchemaGenerator.makeExecutableSchema() fails validation with:
 *
 *   SchemaProblem{errors=[Union type 'Pet' must include one or more member types.]}
 *
 * The validation checks the base definition before merging extension members.
 */
class UnionTest {
    @Test
    fun `test empty union base`() {
        val sdl = """
            type Cat { meow: String }
            type Dog { bark: String }
            union Pet
            extend union Pet = Cat | Dog
            type Query { pet: Pet }
        """.trimIndent()

        val registry = SchemaParser().parse(sdl)

        println("=== Registry parsed successfully ===")
        println("Types: ${registry.types().keys}")
        println("Union extensions: ${registry.unionTypeExtensions()}")

        try {
            val schema = UnExecutableSchemaGenerator.makeUnExecutableSchema(registry)
            println("=== Schema built successfully ===")
        } catch (e: Exception) {
            println("=== Exception ===")
            println("Class: ${e.javaClass.name}")
            println("Message: ${e.message}")
            e.printStackTrace()
            throw e
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions