Four of the rewrite’s directives (@service, @externalField, @condition, @enum) hand the resolution of a slot off to a developer-supplied Java target via the shared ExternalCodeReference input. The mechanics are uniform across all four: a fully-qualified class name from the module’s declared compile classpath, an optional method name that defaults to the GraphQL field name, and an optional argMapping: for renaming Java parameters. This recipe walks through each piece once.
(The deprecated @record directive still parses the same ExternalCodeReference shape, but it is ignored: a class-backed type’s binding is reflected from its producer’s return or parameter type, not from a directive. See How-to: Result-type variants.)
Make the class nameable
A named class must live in this module, in another module of the same Maven project, or in a dependency this module declares itself. A class that is only reachable through another dependency’s dependencies is not nameable: declare it, and it becomes nameable. The generator rejects a schema that names an undeclared class, and the editor marks it before you build.
The recipe is one ordinary <dependency> block on the consumer module:
<dependencies>
<!-- Carries the @service / @externalField / @condition /
@sourceRow / @enum target classes, plus the Java classes that back
class-backed types (reflected from their producers). -->
<dependency>
<groupId>com.example</groupId>
<artifactId>my-services</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
Compile scope is right, not incidental: the generated resolvers call these classes directly, so the consumer module compiles against them and carries them at runtime.
graphitron-sakila-example follows this pattern: the example POM declares graphitron-sakila-service (which carries SampleQueryService, FilmService, InventoryExtensions, etc.) in its outer <dependencies> list.
A <plugin><dependencies> block does not make a class nameable. An earlier revision of this page said the opposite, instructing that the carrying artifact "has to be on the plugin’s classpath" under the graphitron-maven-plugin element’s own <dependencies>. If your build is set up that way, move the <dependency> element from the plugin block to the module’s <dependencies>. The plugin-block route never sufficed on its own: generated code references these classes, so an artifact only the plugin could load already failed the module’s own compile at the first generated reference. What changes with the nameability rule is when you hear about it and what the message says; the failure now happens at the generate that caused it, naming the class and the coordinate to declare.
Reference the class from the schema
className: must be a fully-qualified class name. method: defaults to the GraphQL field name when omitted, so the common case is identity:
type Query {
filmCount: Int!
@service(service: {
className: "com.example.services.SampleQueryService"
# method defaults to "filmCount" (the field name).
})
}
Set method: only when the Java method’s name diverges from the field name:
type Query {
popularFilms(minRentalRate: Float!): [Film!]!
@service(service: {
className: "com.example.services.SampleQueryService",
method: "popularFilmsAboveRate"
})
}
Resolution failures (a class outside the nameable set above, a method not present, a signature that does not match the directive’s expected shape) surface at build time with a directive-specific message: service method could not be resolved — <reason> for @service, and so on. The <reason> names the coordinate to declare when the class sits in an undeclared dependency, the underlying reflection exception when the class or method is missing, or a constraint message when the signature is wrong.
Map GraphQL arguments to Java parameter names
When the GraphQL argument name and the Java parameter name diverge, use argMapping: to rebind. The syntax is "javaParam: path", comma-separated for multiple entries:
type Query {
filmsByServiceRenamed(ids: [Int!]!): [Film!]!
@service(service: {
className: "com.example.services.SampleQueryService",
method: "filmsByServiceRenamed",
argMapping: "filmIds: ids"
})
}
public final class SampleQueryService {
public Result<FilmRecord> filmsByServiceRenamed(DSLContext ctx, List<Integer> filmIds) { ... }
}
The Java parameter filmIds receives the GraphQL argument ids. Unmentioned Java parameters bind to a GraphQL argument of the same name; an empty argMapping: is identity for every parameter. Whitespace around : and , is tolerated, and multi-line text-block input is accepted for long mappings.
The right-hand side is a path rather than a plain name: it may walk into nested input fields (argMapping: "customerId: input.customerId"), the same form at every directive that accepts an argMapping. See Binding a parameter to a nested input field.
argMapping: is meaningful on @service and @condition (the directives whose Java targets receive GraphQL arguments). It is structurally inert on @externalField and @enum, where the Java target consumes no GraphQL-argument-bound parameters; applying it on those two is rejected at parse time.
Per-directive slot names
Each directive’s ExternalCodeReference! parameter has its own name, but the contents are identical:
| Directive | Slot name | Reference page |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
The slot names are historical (each directive was named before the shape was unified); each wraps the class/method pair in an ExternalCodeReference input object.
The deprecated @record directive parses the same record: (ExternalCodeReference) shape, but the binding it once carried is ignored; a class-backed type’s Java class is reflected from its producer instead. A reachable type still carrying @record triggers a build warning telling you to remove it.
Constraints
-
className:must be a fully-qualified class name. Package-prefix short-name resolution is not implemented; spell out the FQCN for now. -
The carrying artifact must be declared in the module’s own
<dependencies>. A<plugin>…<dependencies>block does not make a class nameable, and neither does an artifact arriving transitively through another dependency; see Make the class nameable. -
Reflection runs at code-generation time, not at server startup. A typo in
className:ormethod:fails themvn installthat builds the consumer module, not at request time. -
argMapping:is"javaParam: graphqlArg", not"graphqlArg: javaParam". The Java side comes first because that is what the reflected method signature reads. -
ExternalCodeReference.name:is removed and no longer supported. It was a short alias resolved through the Maven plugin’s<namedReferences>mappings; the argument and that plugin parameter are both gone. A schema still carryingname:fails to load, because the argument is no longer declared on theExternalCodeReferenceinput. Replace each site withclassName:and the fully qualified class name.
See also
-
@serviceis the most common consumer of this surface: a developer-supplied resolver method for a whole field. -
@externalFielduses a static method that returns a jOOQField<T>plugged into the parent’sSELECTprojection. -
@conditionuses a static method that returns a jOOQConditionto splice into a query’sWHEREclause.