Replaces the generated DB operation for a field with a call to an external Java method. Use it when the operation cannot be expressed by the directive surface — multi-statement transactions, business-logic-heavy reads, third-party calls, or shapes the framework does not (yet) cover.

SDL signature

directive @service(
    service: ExternalCodeReference!,
    contextArguments: [String!]
) on FIELD_DEFINITION

input ExternalCodeReference {
    className: String
    method: String
    argMapping: String
}

Parameters

Name Type Default Description

service

ExternalCodeReference!

(required)

The Java target. className is the fully-qualified implementing class (the carrying artifact must be a dependency the module declares itself; see Make the class nameable); method defaults to the GraphQL field name when omitted; argMapping rebinds GraphQL argument names to Java parameter names ("javaParam: graphqlArg, …").

contextArguments

[String!]

none

Names of values pulled from the request GraphQLContext and threaded into the Java method as additional arguments. The runtime must place each named value on the context per request.

Canonical example

The example schema’s root services hand off three different return shapes:

type Query {
    filmsByService(ids: [Int!]!): [Film!]!
        @service(service: {
            className: "no.sikt.graphitron.rewrite.test.services.SampleQueryService",
            method: "filmsByService"
        })

    filmsByServiceRenamed(ids: [Int!]!): [Film!]!
        @service(service: {
            className: "no.sikt.graphitron.rewrite.test.services.SampleQueryService",
            method: "filmsByServiceRenamed",
            argMapping: "filmIds: ids"
        })

    filmCount: Int!
        @service(service: {
            className: "no.sikt.graphitron.rewrite.test.services.SampleQueryService",
            method: "filmCount"
        })
}

filmsByService returns a Result<FilmRecord>, and because Film carries @table the framework reads those records as key carriers: it lifts each record’s primary key and re-selects the requested fields from the table, so the method need only populate the key columns. filmsByServiceRenamed shows argMapping: the GraphQL arg ids binds to the Java parameter filmIds because the service method’s signature reads more naturally that way. filmCount returns a plain scalar.

The Java surface for the first method:

public class SampleQueryService {
    public SampleQueryService(DSLContext context) {  }

    public Result<FilmRecord> filmsByService(List<Integer> ids) {  }
}

The framework constructs the service with whatever it can resolve from the request context (a DSLContext for the request’s database session is the most common parameter); the rest of the method signature is the field’s GraphQL arguments, mapped by name (or by argMapping overrides).

Binding a parameter to a nested input field

An argMapping entry is javaParam: path. The left-hand side names a Java method parameter; the right-hand side names a GraphQL slot in scope, optionally followed by dot-separated segments. The right-hand side works identically on every directive that accepts an argMapping: @service, @condition and @routine. Only the left-hand side differs, because each directive binds a different kind of target.

A dot opens the thing at that position, and what it opens into depends on what the thing is. An input object opens into its fields, which is the case below. An ID carrying @nodeId(typeName:) opens into the key columns of the node type it names, so the segment after it names a key column rather than a field of any SDL type; see Projecting a key column out of a node id. Nothing else opens, and a segment on something that does not is a build error.

The common case is a wrapper input, the shape a Relay-style mutation has: the field declares one input argument and the values the Java method wants live inside it.

input RentFilmInput {
    inventoryId: Int!
    customerId:  Int!
}

type Mutation {
    rentFilm(input: RentFilmInput!): Rental
        @service(service: {
            className:  "com.example.RentalService",
            method:     "rent",
            argMapping: "inventoryId: input.inventoryId, customerId: input.customerId"
        })
}

rent(Integer inventoryId, Integer customerId) receives the two values read out of the input object; the wrapper itself is never handed to the method.

The rules:

  • The head segment (before the first dot) must name a slot in scope at the directive’s site: a GraphQL argument of the field, or the input field itself for an input-field-level @condition. A head naming nothing in scope is a build error that lists the slots that are.

  • Each subsequent segment must name something the value at that depth opens into: a field on the input-object type there, or a key column of the node type where that value is an ID carrying @nodeId(typeName:). A segment naming neither is a build error naming what it looked in and suggesting the near miss.

  • A path may be any depth. input.customer.id walks two levels.

  • Reading is null-safe: if any level along the path is absent, the parameter receives null rather than an error.

  • A bare name with no dots is the single-slot form, and is what an entry without argMapping binds to implicitly. "filmIds: ids" and "filmIds: ids.value" differ only in depth.

Constraints

  • service.className is required. graphql-java rejects a no-arg @service at parse time.

  • service.method defaults to the GraphQL field name. Set it only when the Java method’s name diverges.

  • argMapping is parsed as "javaParam: path" entries, comma-separated. Whitespace around : and , is tolerated; multi-line text-block input is accepted. Empty string is identity. The right-hand side may walk into nested input fields; see Binding a parameter to a nested input field.

  • @service and @mutation are mutually exclusive on the same field. Use @mutation for fully-generated DB writes, @service for custom logic.

  • On non-root fields, @service requires @splitQuery: the parent’s selection set is collected first, the service method receives a Set of parent records (only their primary-key columns populated), and returns a Map from those keys to results. Single-statement nested resolution is not supported on the service path.

  • Conditions and other DB-customisation directives on the same field are ignored: the service is opaque and the generator does not splice generated SQL into a custom method’s body.

See also