Repository navigation
Find union members and interface implementors among sealed subclasses - #854
Open
oryan-block wants to merge 3 commits into
Open
oryan-block wants to merge 3 commits into
oryan-block wants to merge 3 commits into
Conversation
A member of a union or an implementor of an interface that's never returned directly needed a class in the dictionary. When the class found for the union or interface is a Kotlin or Java sealed class, its subclass named after the object type is used instead, with the type arguments of a parameterized sealed class passed on to it. This only runs where the scanner used to fail, after the dictionary, so schemas that build today don't change. When no single subclass matches, the existing error says which sealed class was searched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Java sealed classes need Java 17 to compile, so a profile that's active on Java 17 and later compiles src/test/java17 with release 17. The Java 11 build skips these tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
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.



Partially addresses #200
Checklist
Description
A union member or interface implementor that no resolver returns directly fails the build. In #200 the schema has
union Pet = NormalPet | WeirdPet, but onlypets: [Pet]returns it, and the build fails with:gustavkarlsson hit the same thing with interfaces ("implements a known interface, but no class could be found"). The workarounds in the thread are a dummy query that returns each member, or a dictionary entry per member.
SchemaClassScanneronly finds classes by following resolver return types, plus whatever's in the dictionary. It knows the class it matched toPet, but never looks at that class's subclasses, so once the queue is emptyhandleDictionaryTypesthrows for every member it didn't reach.This only fixes the case where that class is sealed, which is why it doesn't close the issue.
handleDictionaryTypesstill registers dictionary entries first, exactly as before. For each object type that's still without a class,findSealedSubclasslooks at the classes found for its unions and the interfaces it implements, keeps the sealed ones, and picks the single subclass whosesimpleNamematches the type name. That's the same naming conventionSchemaParserDictionaryand the runtime type resolver use. Subclasses of nested sealed classes are included (Pet.Mammal.Dog). If the sealed class is parameterized, a generic subclass gets its arguments viaTypeUtils, soSuccess<T>underResult<Pet>is scanned asSuccess<Pet>. Matches are registered with a newSealedSubclassReferenceand then scanned like any other class, so a member reachable only from another member's fields is found too.All matches in a step are registered before any of them is scanned. Otherwise the order of the union's members would decide whether a different class found later for one of them gets reported. The step only throws when none of the missing types got a match. Types still missing after scanning the matches fail on the next pass. I put the lookup inside the step that used to throw, not in a separate final step, so nothing changes for schemas that build today, and kotlin-reflect isn't called for them.
I used
KClass.sealedSubclassesrather thanClass#getPermittedSubclasses. The build baseline is Java 11, and Kotlin classes compiled forjvmTarget1.8 have noPermittedSubclassesattribute, so the JDK API would miss them. kotlin-reflect also returns the permitted subclasses of Java sealed types, which the Java 17 tests check.When a union or interface has a sealed class but no single subclass matches, the existing message now ends with a hint: "No subclass of sealed type X is named 'Y'." or "More than one subclass of sealed type X is named 'Y': a, b." It's appended after two spaces, so the start of the message is unchanged.
Left out:
@JsonSubTypes. I didn't take it. The JSON type id isn't the GraphQL type name, and it would make a serialization annotation part of the schema mapping. It's the only route that would help non-sealed classes on Java 11, though, so it'd be a reasonable follow-up feeding the same lookup.includeUnusedTypesor interface fields still have no class to inspect.SchemaClassScannerSealedTypesTesthas 12 tests. Where the schema builds, they run a real query. They cover a union over a Kotlin sealed class (the #200 case), an interface over a sealed interface, nested sealed classes, a generic sealed class, a member only reachable by scanning a sealed subclass, the dictionary overriding a sealed subclass, a conflicting scanned class giving "Two different classes used for type Author" whatever the member order, the hint for a union, a partly matched union and an interface implementor, and the ambiguous-name hint. One test is a guard that a non-sealed class still gets the exact existing message.SchemaClassScannerSealedJavaTypesTesthas two JUnit tests, a Java sealed interface with records and a Java sealed abstract class. It lives insrc/test/java17and is compiled by a newjava17-testsprofile inpom.xml, active on JDK 17 and later, with its owntestCompileexecution usingrelease 17. The default javac and Kotlin compiles don't see that directory. I ran the first ten Kotlin tests and both Java tests against master before changing the scanner: all of them failed except the non-sealed guard. The two hint tests added afterwards assert the exact message with the hint, which master doesn't append.clean verifypasses on JDK 25 (272 tests, including the Java 17 ones) and passed on JDK 11 before the last two Kotlin tests were added, with the profile inactive.It touches
SchemaClassScannerlike #842, but in a different function, and it merges cleanly with that branch.Behaviour change: a union or interface whose class is a Kotlin sealed class or interface, or a Java 17 sealed type, now builds when members or implementors are only reachable through it. Schemas that build today are unchanged, and failing schemas without a sealed class get exactly the same error. Failing schemas with one get the hint appended. The precedence is classes already found by scanning, then the dictionary, then sealed subclasses. A different class found later for a sealed match fails with the usual "Two different classes used for type X", naming "subclass of sealed type ...". No public API or options change.
🤖 Generated with Claude Code