Repository navigation
Build enums used only by directives without a Java class - #852
Merged
Merged
Conversation
An enum used only as a directive argument, such as Federation 2's @link(for: Purpose), failed the schema build with "Enum type 'Purpose' is used in a directive, but no class could be found" unless a Java enum was passed in the dictionary. That happened even when the directive was never applied, since the scanner checks the arguments of every directive definition, and createEnumObject could only build an enum from a class. Directive argument values are never passed to resolver methods, so such an enum doesn't need a class. When the scanner finds no class for it, the enum is now built with its value names as values, which is what graphql-java does without an EnumValuesProvider. Applied directives and SchemaDirectiveWiring implementations then get "SECURITY" from getValue() rather than an enum constant. Passing the enum class in the dictionary still gives enum constants. The decision is made once scanning is done, so a class found later, for example through an unused type with includeUnusedTypes, is still used. Enums that fields or arguments use keep their class as before, and an enum matched to String, Object or a Map still fails in the parser. Federation's bundled definitions still need their custom scalars, such as Import and FieldSet, passed to the parser. One case does change for resolvers. An enum that a directive uses and that is also used by an input object field without a Java property, e.g. an input bound to a Map, now builds with the value names, so the Map gets "SECURITY". Without the directive that schema still fails. A test pins this. Fixes #823 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit built an enum from its value names whenever a directive used it and the scanner found no class for it. That also covered enums whose values do reach user code without going through a resolver parameter the scanner can match: input fields with no Java property, such as an input bound to a Map, and arguments of unimplemented fields served by a missing resolver data fetcher. Those schemas fail without the directive, but declaring an unrelated directive made them build, and the Map or the data fetcher got "SECURITY" instead of an enum constant. Now an enum is only built from its value names if no field, argument or input field of a type in the schema uses it, including fields added by extensions and types found later through includeUnusedTypes. Otherwise it needs a class as before, and the parser fails with "Expected enum with name 'Purpose' but found none!". This replaces the check against the scanner's dictionary, since any enum the scanner matched to a class was matched through such a field. The test that pinned the Map-bound input case now expects the error, and new tests cover an unimplemented field's argument added by an extension and an unused type's Object property. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only handleDirectiveEnum puts enums in unvalidatedTypes, so the separate directiveEnumsWithoutClass set isn't needed. validateAndCreateResult now filters the enums in unvalidatedTypes instead. createEnumObject unwraps the enum class once. The failure cases are now one test with an assertSchemaError helper, and the tests share the issue's enum and directive. Dropped the Fed2 link test, since its extra arguments don't touch enums, the test for an enum used by a directive and a resolver, which "scanner should handle directives with enum input value" already covers, and the field directive in the applied directives test, which goes through the same code as the schema one. No behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
oryan-block
added a commit
that referenced
this pull request
Oct 17, 2026
The directive enum test from #852 expected the parser's "Expected enum with name 'Purpose' but found none!" for an enum that's also used by an input field bound to a Map. With this branch the scanner already fails for that enum, since no class can be found for it, and names the input type, the field and how to fix it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Fixes #823
Checklist
Description
An enum that's only used as a directive argument fails the schema build unless you register a Java enum for it. This is the schema from the issue:
It fails even when the directive is never applied. The common case is Fed2's
@link(for: link__Purpose), where people end up writing a dummylink__Purposeenum just to get the schema to build.SchemaClassScanner#handleDirectivesscans the arguments of every directive definition and sent enums throughhandleDictionaryTypes, which throws when the dictionary has no class for them.SchemaParser#createEnumObjectalso required a Java enum for every enum. But directive argument values are never passed to resolver methods, so there's nothing for the class to be matched against.Now the scanner handles directive enums in
handleDirectiveEnum. If there's a dictionary class for the enum it's used, as before. Otherwise the enum goes intounvalidatedTypes, which nothing else puts enums in. Once scanning is done,validateAndCreateResultkeeps only the enums there that no field, argument or input field of a type in the schema uses, and passes them to the parser asScannedSchemaObjects#directiveOnlyEnums. That check includes fields added byextend type/extend inputand types found throughincludeUnusedTypes.createEnumObjectbuilds those enums with their value names as values, which is what graphql-java does for enums without a class. "Expected enum with name" and "is not a Java enum" still apply to every other enum.The field-use check came out of review. My first version built any classless directive enum from its names, even when a
Map-bound input field or the argument of a field with no resolver also used it. That meant adding an unrelated directive could turn a schema that failed to build into one that built and handedStrings to user code. Now any use outside directive definitions needs a class, the same as on master.Left out:
Map-bound input case a clearer message.extend enumis still ignored, the same as for enums backed by a class.createDirectives,determineInputTypeandbuildAppliedDirectivesaren't touched, so the overlap with #843 is a few nearby lines inSchemaParser. #842 will have a small textual conflict just abovevalidateAndCreateResult, where both add code. Both PRs fail on an enum used by aMap-bound input field, but #842 throws its ownSchemaClassScannerErrorbefore the parser runs. Whichever lands second has to update the expected exception for theMapcase inscanner requires a Java enum for enums used by directives and by fields.There are 5 new tests in
SchemaClassScannerDirectiveTest. The build tests cover the issue's schema exactly as written, applied schema directives using the enum's default and an explicit value (checked throughgetValue(), a full introspection query andSchemaPrinter), a dictionary class for a directive-only enum, and an enum class found through an unused type withincludeUnusedTypes. The last test holds the guards: aStringparameter still gives "is not a Java enum", and anObjectparameter, anAny?property on an unused type, aMap-bound input field and an unimplemented field's argument inextend type Querystill give "Expected enum with name". The existingscanner should handle directives with enum input valuealready covers an enum used by both a directive and a resolver. I ran the test file against master: the issue schema, applied directives and unused type tests fail with the error above, and the guards test fails at the unused type case, since master throws its scanner error, not the parser's. The dictionary test and the existing tests pass on master.clean verifypasses.Behaviour change: an enum that only directive definitions use, with no dictionary class, now builds instead of failing. Its values are the value names as
Strings, soappliedDirective.getArgument("for").getValue<...>()in aSchemaDirectiveWiringreturns"SECURITY", not an enum constant. Passing the enum in the dictionary (.dictionary(Purpose::class)) brings the constants back. These enums are now in the built schema and no longer show up ingetUnusedDefinitions(). The scanner error "Enum type '...' is used in a directive, but no class could be found for that type name" is gone. Schemas that hit it because the enum is also used by a field without a class still fail, now with the parser's "Expected enum with name 'Purpose' but found none!". Fed2 users who load federation's definitions still have to pass its custom scalars (Import,FieldSetand so on) to the parser.🤖 Generated with Claude Code