Skip to content

Commit 23c59ae

Browse files
Raymie Stataclaude
andcommitted
Add type resolver validation to FastBuilder
FastBuilder now validates that all interfaces and unions have type resolvers, matching the behavior of the standard GraphQLSchema.Builder. This validation is always performed regardless of the withValidation() setting, which only controls GraphQL spec validation via SchemaValidator. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 131f169 commit 23c59ae

2 files changed

Lines changed: 98 additions & 13 deletions

File tree

src/main/java/graphql/schema/GraphQLSchema.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,19 +1413,31 @@ public FastBuilder withValidation(boolean enabled) {
14131413
*
14141414
* @return the built schema
14151415
* @throws InvalidSchemaException if validation is enabled and the schema is invalid
1416-
* @throws AssertException if a type reference cannot be resolved
1416+
* @throws AssertException if a type reference cannot be resolved or if an interface/union
1417+
* type is missing a type resolver
14171418
*/
14181419
public GraphQLSchema build() {
1419-
// Step 1: Replace type references
1420+
// Validate type resolvers for all interfaces and unions
1421+
for (GraphQLNamedType type : typeMap.values()) {
1422+
if (type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType) {
1423+
String typeName = type.getName();
1424+
if (!codeRegistryBuilder.hasTypeResolver(typeName)) {
1425+
String typeKind = type instanceof GraphQLInterfaceType ? "interface" : "union";
1426+
assertShouldNeverHappen("You MUST provide a type resolver for the %s type '%s'", typeKind, typeName);
1427+
}
1428+
}
1429+
}
1430+
1431+
// Replace type references
14201432
shallowTypeRefCollector.replaceTypes(typeMap);
14211433

1422-
// Step 2: Add built-in directives if missing
1434+
// Add built-in directives if missing
14231435
Directives.BUILT_IN_DIRECTIVES.forEach(this::addDirectiveIfMissing);
14241436

1425-
// Step 3: Create schema via private constructor
1437+
// Create schema via private constructor
14261438
GraphQLSchema schema = new GraphQLSchema(this);
14271439

1428-
// Step 4: Optional validation
1440+
// Optional GraphQL spec validation
14291441
if (validationEnabled) {
14301442
Collection<SchemaValidationError> errors = new SchemaValidator().validateSchema(schema);
14311443
if (!errors.isEmpty()) {

src/test/groovy/graphql/schema/FastBuilderTest.groovy

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,9 +1067,13 @@ class FastBuilderTest extends Specification {
10671067
.type(postType))
10681068
.build()
10691069

1070+
and: "code registry with type resolver"
1071+
def codeRegistry = GraphQLCodeRegistry.newCodeRegistry()
1072+
.typeResolver("Node", { env -> null })
1073+
10701074
when: "building with FastBuilder"
10711075
def schema = new GraphQLSchema.FastBuilder(
1072-
GraphQLCodeRegistry.newCodeRegistry(), queryType, null, null)
1076+
codeRegistry, queryType, null, null)
10731077
.addType(nodeInterface)
10741078
.addType(postType)
10751079
.build()
@@ -1120,9 +1124,13 @@ class FastBuilderTest extends Specification {
11201124
.type(entityInterface))
11211125
.build()
11221126

1127+
and: "code registry with type resolver"
1128+
def codeRegistry = GraphQLCodeRegistry.newCodeRegistry()
1129+
.typeResolver("Entity", { env -> null })
1130+
11231131
when: "building with FastBuilder"
11241132
def schema = new GraphQLSchema.FastBuilder(
1125-
GraphQLCodeRegistry.newCodeRegistry(), queryType, null, null)
1133+
codeRegistry, queryType, null, null)
11261134
.addType(entityInterface)
11271135
.addType(userType)
11281136
.addType(productType)
@@ -1686,6 +1694,42 @@ class FastBuilderTest extends Specification {
16861694
schema.codeRegistry.getTypeResolver(resolvedUnion) != null
16871695
}
16881696

1697+
def "union without type resolver throws error"() {
1698+
given: "a union without type resolver"
1699+
def catType = newObject()
1700+
.name("Cat")
1701+
.field(newFieldDefinition()
1702+
.name("meow")
1703+
.type(GraphQLString))
1704+
.build()
1705+
1706+
def petUnion = GraphQLUnionType.newUnionType()
1707+
.name("Pet")
1708+
.possibleType(catType)
1709+
// No type resolver!
1710+
.build()
1711+
1712+
and: "a query type"
1713+
def queryType = newObject()
1714+
.name("Query")
1715+
.field(newFieldDefinition()
1716+
.name("pet")
1717+
.type(petUnion))
1718+
.build()
1719+
1720+
when: "building without type resolver"
1721+
new GraphQLSchema.FastBuilder(
1722+
GraphQLCodeRegistry.newCodeRegistry(), queryType, null, null)
1723+
.addType(catType)
1724+
.addType(petUnion)
1725+
.build()
1726+
1727+
then: "error is thrown"
1728+
def e = thrown(AssertException)
1729+
e.message.contains("MUST provide a type resolver")
1730+
e.message.contains("Pet")
1731+
}
1732+
16891733
def "union with missing member type reference throws error"() {
16901734
given: "a union with missing type reference"
16911735
def petUnion = GraphQLUnionType.newUnionType()
@@ -1780,7 +1824,7 @@ class FastBuilderTest extends Specification {
17801824
resolvedApplied.getArgument("info").getType() == metaScalar
17811825
}
17821826

1783-
def "withValidation(false) allows schema without type resolver"() {
1827+
def "interface without type resolver throws error"() {
17841828
given: "an interface without type resolver"
17851829
def nodeInterface = GraphQLInterfaceType.newInterface()
17861830
.name("Node")
@@ -1798,16 +1842,45 @@ class FastBuilderTest extends Specification {
17981842
.type(nodeInterface))
17991843
.build()
18001844

1801-
when: "building with validation disabled"
1802-
def schema = new GraphQLSchema.FastBuilder(
1845+
when: "building without type resolver"
1846+
new GraphQLSchema.FastBuilder(
1847+
GraphQLCodeRegistry.newCodeRegistry(), queryType, null, null)
1848+
.addType(nodeInterface)
1849+
.build()
1850+
1851+
then: "error is thrown"
1852+
def e = thrown(AssertException)
1853+
e.message.contains("MUST provide a type resolver")
1854+
e.message.contains("Node")
1855+
}
1856+
1857+
def "withValidation(false) still requires type resolvers"() {
1858+
given: "an interface without type resolver"
1859+
def nodeInterface = GraphQLInterfaceType.newInterface()
1860+
.name("Node")
1861+
.field(newFieldDefinition()
1862+
.name("id")
1863+
.type(GraphQLString))
1864+
.build()
1865+
1866+
and: "a query type"
1867+
def queryType = newObject()
1868+
.name("Query")
1869+
.field(newFieldDefinition()
1870+
.name("node")
1871+
.type(nodeInterface))
1872+
.build()
1873+
1874+
when: "building with validation disabled but no type resolver"
1875+
new GraphQLSchema.FastBuilder(
18031876
GraphQLCodeRegistry.newCodeRegistry(), queryType, null, null)
18041877
.addType(nodeInterface)
18051878
.withValidation(false)
18061879
.build()
18071880

1808-
then: "schema builds without error"
1809-
schema != null
1810-
schema.getType("Node") instanceof GraphQLInterfaceType
1881+
then: "error is still thrown for missing type resolver"
1882+
def e = thrown(AssertException)
1883+
e.message.contains("MUST provide a type resolver")
18111884
}
18121885

18131886
def "withValidation(true) rejects schema with incomplete interface implementation"() {

0 commit comments

Comments
 (0)