Three hand-written-feeling classes ship as part of every Graphitron code-generation run. They aren’t pulled in from a runtime library; they are emitted into your <outputPackage> source tree alongside the type-specific fetchers, and your consumer module compiles them. This is what keeps the runtime dependency surface flat: your code depends on jOOQ, graphql-java, and your own generated sources, nothing else.

This page documents the public method surface of all three. The directive reference under reference/directives covers what gets emitted because of SDL declarations; this page covers what gets emitted unconditionally and how your consumer code calls into it.

Graphitron facade

Emitted by GraphitronFacadeGenerator into <outputPackage>. Single public entry point for building the schema:

public final class Graphitron {

    public static GraphQLSchema buildSchema(
        Consumer<GraphQLSchema.Builder> customizer);

    // Emitted only when your SDL declares a federation @link.
    public static GraphQLSchema buildSchema(
        Consumer<GraphQLSchema.Builder> schemaCustomizer,
        Consumer<SchemaTransformer>     federationCustomizer);
}

buildSchema returns a fully wired GraphQLSchema: every type, field, fetcher, and directive Graphitron understands is attached before the schema is built. Custom scalars are wired automatically from the @scalarType(scalar:) directive on each scalar declaration (see @scalarType directive); the customizer lambda receives the underlying GraphQLSchema.Builder so consumer code can layer in additional non-scalar types, hand-rolled type resolvers, or custom directives before the build.

Customizer contract

The customizer is additive only. The following are unsupported and break the assembled schema:

  • .query(…​) / .mutation(…​) / .subscription(…​) — replaces the root types Graphitron just wired.

  • .clearDirectives() — drops the directive set Graphitron emits for federation, authorization, and validation.

  • .codeRegistry(GraphQLCodeRegistry) (the replace overload) — replaces the fetcher registry Graphitron just populated.

The supported extension shape for codeRegistry is the transform overload .codeRegistry(UnaryOperator<GraphQLCodeRegistry.Builder>); that one is fine and is the seam for adding type resolvers to interfaces and unions whose Java types Graphitron cannot infer (hand-rolled domain types, types pulled in from a non-Graphitron source, polymorphic shapes outside the catalog). How-to: Polymorphic types covers the resolver shape.

The customizer runs once per schema build, not once per request. Per-request runtime values flow through GraphitronContext on the ExecutionInput.graphQLContext, not through this lambda.

Federation two-arg overload

When at least one of your .graphqls files carries an Apollo Federation @link, the generator emits a second overload that exposes the federation builder. The federation customizer’s most useful method is fetchEntities(…​) for swapping in a hand-rolled entity fetcher:

GraphQLSchema schema = Graphitron.buildSchema(
    b   -> {},
    fed -> fed.fetchEntities(myCustomFetcher));

Without the @link, the federation overload isn’t emitted, the federation surface stays out of the build entirely, and buildSchema returns a vanilla schema. How-to: Apollo Federation transport covers the transport-side wiring; How-to: Federation key fields covers entity-side declaration.

GraphitronContext interface

Emitted by GraphitronContextInterfaceGenerator into <outputPackage>.schema. The sealed per-request contract every generated DataFetcher reads from. Apps no longer implement it; the only impl is the generator’s own GraphitronContextImpl singleton (nested inside the interface for same-compilation-unit permits), populated by Graphitron.newExecutionInput(…​).

public sealed interface GraphitronContext {

    default DSLContext getDslContext(DataFetchingEnvironment env) {
        return env.getGraphQlContext().get(DSLContext.class);
    }

    default Object getContextArgument(DataFetchingEnvironment env, String name) {
        Object value = env.getGraphQlContext().get(name);
        if (value == null) {
            throw new IllegalStateException("context value '" + name
                + "' was not supplied; call Graphitron.newExecutionInput(...) to populate it");
        }
        return value;
    }

    default Validator getValidator(DataFetchingEnvironment env) {
        return DefaultValidatorHolder.INSTANCE;
    }

    final class GraphitronContextImpl implements GraphitronContext {
        public static final GraphitronContextImpl INSTANCE = new GraphitronContextImpl();
        private GraphitronContextImpl() {}
    }
}

The factory IS the registration point; the parameter list is schema-driven (DSLContext first, then one parameter per contextArgument named in @service / @tableMethod / @condition directives, alphabetically):

ExecutionInput input = Graphitron.newExecutionInput(dsl, /* contextArgs... */)
    .query(query)
    .build();

Generated fetchers retrieve the impl through a private helper emitted once per *Fetchers class (graphitronContext(env)); generated helpers that issue SQL on behalf of those fetchers (for example, ConnectionHelper.totalCount(env) for @asConnection connections) emit the same shim. The sealed-interface + singleton-impl pair means GraphitronContext is global to the build, not per-request; all per-request state lives in the GraphQLContext the factory populates.

getDslContext

Reads the per-request DSLContext from env.getGraphQlContext().get(DSLContext.class), populated by the factory’s defaultDsl parameter. The default impl is the only impl; this is not an override point. To return a tenant-specific DSLContext per request, build the right DSLContext at request entry and pass it to Graphitron.newExecutionInput(dsl, …​) (reintroduced under R45’s tenant-column work).

getContextArgument

Reads the value from env.getGraphQlContext() (under the contextArgument’s string name) and returns it as Object; the cast to the expected Java type happens at the generated call site ((String) graphitronContext(env).getContextArgument(env, "userId")). A missing entry throws IllegalStateException naming the contextArgument and pointing the reader at Graphitron.newExecutionInput(…​); a wrong-typed entry surfaces as ClassCastException at the generated cast. Both throw paths are only reachable when a consumer hand-rolls an ExecutionInput.Builder outside the factory; going through Graphitron.newExecutionInput(…​) makes the same mistake a compile error at the call site, because the factory’s typed parameter slot IS the reflected expected type. The framework’s redact path replaces the prose message with a correlation-id reference before it reaches the consumer, so the runtime throw is server-log surface only; the typed factory is the load-bearing diagnostic.

How-to: Stacking and overriding conditions covers the call-site shape; the supplying side is the factory’s typed parameter slot.

getValidator

Returns the jakarta.validation.Validator the wrapper’s pre-execution validation step uses for this fetch. Defaults to the JVM’s default validator factory:

default Validator getValidator(DataFetchingEnvironment env) {
    return DefaultValidatorHolder.INSTANCE;
}

DefaultValidatorHolder is a holder-class idiom inside the same emitted file; its INSTANCE is initialised on the first call to getValidator (Validation.buildDefaultValidatorFactory().getValidator()) and shared by every subsequent default-using request. The method is no longer an override point on the sealed interface; custom-validator-factory configuration ships under R192 as a Mojo-driven hook.

NodeIdEncoder utility

Emitted by NodeIdEncoderClassGenerator into <outputPackage>.schema only when at least one type carries @node. Static, non-extendable encode/decode helpers for Relay global IDs:

public final class NodeIdEncoder {

    public static String encode(String typeId, Object... values);

    public static String peekTypeId(String base64Id);

    // Per-Node helpers, one pair per @node type:
    public static String  encode<TypeName>(T1 v1, ..., TN vN);
    public static RecordN<T1, ..., TN> decode<TypeName>(String base64Id);
}

Wire format

Every Graphitron-emitted nodeId is base64-url-encoded "typeId:v1,v2,…​" (no padding, UTF-8). Commas inside values are escaped as %2C, so a key value containing a literal comma round-trips losslessly. The format matches the legacy no.sikt.graphql.NodeIdStrategy encoding, so IDs round-trip across a cut-over from the legacy generator.

encode(typeId, values…​) returns null when any value is null, so the GraphQL field resolves to null rather than emitting a malformed ID.

Per-Node helpers

For each @node-decorated type the generator emits two static helpers named after the GraphQL type:

  • encode<TypeName>(T1 v1, …​, TN vN) — bakes the typeId into the helper name so call sites pass typed key values directly. Returns null if any value is null.

  • decode<TypeName>(String base64Id) — returns a jOOQ RecordN<T1, …​, TN> typed against the bound table’s PK columns, or null uniformly on malformed input or a typeId mismatch. Carrier consumers wrap that null through the matching CallSiteExtraction.NodeIdDecodeKeys.* arm.

peekTypeId(base64Id) reads the typeId off the wire string without decoding the rest. Query.node(id:) uses this to dispatch to the right @node type’s table; a non-typed reader can call it the same way.

Why a final class with private constructor

A single canonical wire format across every generated dispatcher is what makes nodeIds durable across schema evolution. Subclassing the encoder to override the encoding would let one corner of an app speak a different dialect from another, and the result would not round-trip. How-to: Global object IDs covers Relay Node end-to-end, including stable-ID strategies and keyColumns migrations.

See also

  • Directive reference is the SDL-driven half of what generated code looks like; this page is the SDL-independent half.

  • How-to: Test your schema covers wiring Graphitron.buildSchema(…​) and a request-scoped GraphitronContext into an execution-tier test.

  • How-to: Custom scalars covers declaring a scalar with @scalarType(scalar:) and authoring the GraphQLScalarType constant the directive points at.

  • How-to: Tenant scoping covers per-request DSLContext routing (recipe is being rewritten under R45; the link resolves but the body shows a deferral banner).

  • How-to: Global object IDs covers NodeIdEncoder end-to-end, including the per-Node helpers, keyColumns migrations, and Query.node / Query.nodes dispatch.