Parameterize the @Scaffold service superclass so scaffolded service methods aren't type-erased#15717
Open
codeconsole wants to merge 2 commits into
Open
Parameterize the @Scaffold service superclass so scaffolded service methods aren't type-erased#15717codeconsole wants to merge 2 commits into
codeconsole wants to merge 2 commits into
Conversation
…thods aren't type-erased ScaffoldingServiceInjector set a RAW superclass (extends GormMongoService), because it built the node via valueClassNode.getPlainNodeReference() then nonGeneric(raw, domain) — which no-ops since the type already has no generics. Result: inherited get():T/list():List<T>/save(T):T erased to the GormEntity upper bound, forcing casts/@CompileDynamic at every statically-compiled call site. Set the superclass to a parameterized GormMongoService<Domain> instead.
jdaugherty
reviewed
Jun 5, 2026
Contributor
jdaugherty
left a comment
There was a problem hiding this comment.
Let's add a test that makes use of this.
…le form + add tests The previous change parameterized the @scaffold superclass node, but the generic signature only survived for the generic form (@scaffold(GormService<Widget>)), where the source usage already flagged the class as using generics. The simple form (@scaffold(Widget)) still compiled to a raw superclass, so inherited get():T/list():List<T>/save(T):T erased to GormEntity under static compilation. Injection runs at CANONICALIZATION (after generics resolution), so the class-level generic signature is only written at generation time when the class node reports usesGenerics(). Set classNode.setUsingGenerics(true) so the parameterized superclass is emitted for every form, while preserving custom bases (e.g. GormMongoService<Schedule>). Add ScaffoldingServiceInjectorSpec covering the simple form, the generic form, a custom scaffold base, and static-compilation resolution of get/list/save.
✅ All tests passed ✅🏷️ Commit: 5b905eb Learn more about TestLens at testlens.app. |
Contributor
Author
@jdaugherty done |
sbglasius
approved these changes
Jun 5, 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.
Problem
A scaffolded service declared with a parameterized base, e.g.
@Scaffold(GormMongoService<Book>), is given the raw typeGormService/GormMongoServiceas its superclass — the<Book>type argument is dropped. So every inherited generic method (get():T,list():List<T>,save(T):T, …) resolves to theGormEntityupper bound rather than the concrete domain type.Under
@GrailsCompileStatic/@CompileStaticthis forces a cast at every call site:Root cause
In
ScaffoldingServiceInjector.performInjectionOnAnnotatedClass:getPlainNodeReference()removes the generics, so the followingnonGeneric(raw, domain)is a no-op:replaceGenericsPlaceholdersearly-returns the plain node because the type is no longer using generics. The domain type is known but never applied to the superclass.Fix
Set the superclass to a parameterized
GormService<Domain>:Impact
Inherited scaffold-service methods now resolve to the domain type with no casts under static compilation:
Generics are compile-time only, so there is no runtime behavior change. Verified against a real app's
@Scaffold(GormMongoService<T>)services —get()/list()compile to the domain type under@GrailsCompileStaticwith zero casts (previously every such call required a cast or@CompileDynamic).