Six of the rewrite’s directives (@service, @externalField, @tableMethod, @condition, @enum, @record) hand the resolution of a slot off to a developer-supplied Java target via the shared ExternalCodeReference input. The mechanics are uniform across all six: a fully-qualified class name on the rewrite plugin’s 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.

@sourceRow carries the same plugin-classpath requirement but exposes the lifter via flat (className, method) args rather than the ExternalCodeReference wrapper. See Source-row directive for the dedicated walk-through.

Make the class reachable

The rewrite’s Maven plugin reflects on the named class at code-generation time (via Class.forName(…​)). The class therefore has to be on the plugin’s classpath, not the consumer module’s compile classpath. Declare the carrying artifact under the plugin’s own <dependencies> block:

<plugin>
    <groupId>no.sikt</groupId>
    <artifactId>graphitron-maven-plugin</artifactId>
    <version>${graphitron.version}</version>
    <executions>
        <execution>
            <goals><goal>generate</goal></goals>
        </execution>
    </executions>
    <dependencies>
        <!-- Carries the @service / @externalField / @tableMethod / @condition /
             @sourceRow / @enum / @record target classes. -->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>my-services</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>
</plugin>

The dependency is plugin-scoped: the consumer module never imports the artifact at compile time (the generator emits sources that wire in via DSLContext-mediated reflection bindings, not via direct import statements). Putting the artifact in the outer <dependencies> block instead would compile the consumer module against it for no benefit and pollute the runtime jar.

graphitron-sakila-example follows this pattern: the example POM declares graphitron-sakila-service (which carries SampleQueryService, FilmService, InventoryExtensions, etc.) under the <plugin>…​<dependencies> block of the graphitron-maven-plugin plugin in graphitron-rewrite/graphitron-sakila-example/pom.xml, not under the module’s outer dependency list.

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!]!
        @tableMethod(
            className: "com.example.services.SampleQueryService",
            method:    "popularFilmsAboveRate"
        )
}

Reflection failures (class not on the plugin classpath, method not present, signature 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, table method could not be resolved — <reason> for @tableMethod, and so on. The <reason> is 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: graphqlArg", 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.

argMapping: is meaningful on @service, @tableMethod, and @condition (the directives whose Java targets receive GraphQL arguments). It is structurally inert on @externalField, @enum, and @record, where the Java target consumes no GraphQL-argument-bound parameters; applying it on those three 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

@service

service: (ExternalCodeReference)

@service

@externalField

reference: (ExternalCodeReference)

@externalField

@tableMethod

flat className: + method: + argMapping:

@tableMethod

@condition

condition: (ExternalCodeReference)

@condition

@enum

enumReference: (ExternalCodeReference)

@enum

@record

record: (ExternalCodeReference)

@record

@sourceRow

flat className: + method:

@sourceRow

The slot names are historical (each directive was named before the shape was unified). The two flat-form directives (@tableMethod, @sourceRow) carry className: and method: directly on the directive; the rest still wrap them in an ExternalCodeReference input object.

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 on the plugin’s classpath (the <plugin>…​<dependencies> block), not the module’s outer <dependencies>. The generator runs at build time and only sees what the plugin can load.

  • Reflection runs at code-generation time, not at server startup. A typo in className: or method: fails the mvn install that 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: (a deprecated short alias resolved via the Mojo’s <namedReferences> config) is still parsed for legacy schemas in transition, but new schemas should use className: directly. The deprecation warning surfaces during the rewrite-generate Maven goal.

See also

  • @service is the most common consumer of this surface: a developer-supplied resolver method for a whole field.

  • @externalField uses a static method that returns a jOOQ Field<T> plugged into the parent’s SELECT projection.

  • @tableMethod uses a static method that returns a jOOQ Table<R> chosen at request time.

  • @condition uses a static method that returns a jOOQ Condition to splice into a query’s WHERE clause.

  • Source-row directive (@sourceRow) uses a static method that supplies the per-parent batch key for a @record-typed parent. Sibling pattern with flat (className, method) args instead of the ExternalCodeReference wrapper documented above.