GraphQL leans on a small set of built-in scalars (Int, Float, String, Boolean, ID); real schemas usually need more (UUID, Date, DateTime, JSON, Long). Graphitron itself never coerces values; it leaves coercion to graphql-java by emitting GraphQLTypeReference.typeRef("<name>") everywhere a scalar is referenced. The reference is resolved against a GraphQLScalarType registered on the synthesized schema, and Graphitron’s scalar resolver does that registration for you so the buildSchema(…) customizer does not have to.
This recipe walks through declaring the scalar in the SDL, pointing the @scalarType(scalar:) directive at a public static final GraphQLScalarType constant on the classpath, and the convention-table shortcut that lets you omit the directive entirely for shapes covered by graphql-java-extended-scalars. The full directive surface (per-arm rejection reasons, LSP completion, classpath-visibility rules) is on the @scalarType directive reference; this recipe is the worked example.
Declare the scalar and bind it in one place
Add a scalar declaration to one of your .graphqls files and attach @scalarType(scalar: "FQN.FIELD") pointing at the constant you want the schema to use. The fully-qualified reference takes the form package.Class.FIELD; inner classes use $ (pkg.Outer$Inner.FIELD).
scalar UUID @scalarType(scalar: "graphql.scalars.ExtendedScalars.UUID")
scalar DateTime @scalarType(scalar: "graphql.scalars.ExtendedScalars.DateTime")
type Customer @table(name: "customer") {
id: UUID @field(name: "customer_id")
createdAt: DateTime @field(name: "create_date")
}
Graphitron loads each named class via the project-aware codegen classloader, reads the static final field, and reflects on the bound Coercing<I, O> to recover the Java type used for input-record components, service parameters, and Field<X> projections. The constant is then registered on the synthesized schema (.additionalType(<constant>)) automatically. Consumer code does not need to wire the scalar in its buildSchema(…) hook.
|
The class the directive points at must be on the codegen classpath: the consumer module’s |
Skip the directive when extended-scalars covers the shape
graphql-java-extended-scalars ships ready-made constants for most common shapes. If the SDL name matches one of the 30 entries in graphitron’s convention table and graphql.scalars.ExtendedScalars is on the consumer’s compile classpath, the directive is optional: a bare scalar UUID resolves to ExtendedScalars.UUID automatically. Both the bare (scalar BigDecimal) and GraphQL-prefixed (scalar GraphQLBigDecimal) names map to the same constant.
# Resolves to ExtendedScalars.UUID via the convention table:
scalar UUID
# Same, but explicit — useful as the canonical form when you want the
# resolution path to be obvious at read time:
scalar UUID @scalarType(scalar: "graphql.scalars.ExtendedScalars.UUID")
Pick whichever form you prefer. The directive wins over the convention layer, so a consumer who imports extended-scalars but wants BigDecimal modeled as their own Money wrapper just writes:
scalar BigDecimal @scalarType(scalar: "com.example.Scalars.MONEY")
and the directive’s MONEY constant beats the conventional ExtendedScalars.GraphQLBigDecimal.
Build your own GraphQLScalarType for a unique domain shape
When no library covers the shape, author the constant in your consumer module and point @scalarType at it. The constant must be public static final, non-null at code generation, and assignable to graphql.schema.GraphQLScalarType. The bound Coercing<I, O> must declare concrete type parameters (a raw Coercing, Coercing<Object, Object>, or an anonymous-class declaration whose I erases to Object is rejected with a CoercingErased reason).
package com.example.scalars;
import graphql.schema.Coercing;
import graphql.schema.GraphQLScalarType;
public final class Scalars {
public static final GraphQLScalarType EPOCH_SECONDS = GraphQLScalarType.newScalar()
.name("EpochSeconds")
.description("Unix epoch seconds, signed 64-bit.")
.coercing(new Coercing<Long, Long>() { /* serialize / parseValue / parseLiteral */ })
.build();
private Scalars() {}
}
Then point the SDL declaration at the constant:
scalar EpochSeconds @scalarType(scalar: "com.example.scalars.Scalars.EPOCH_SECONDS")
The Coercing<Internal, External> parameters let you choose the Java side (Internal) and the wire side (External) independently; graphql-java’s javadoc covers the contract for each method. name() must match the SDL scalar declaration; the runtime resolves typeRef("EpochSeconds") against this exact name. Once the directive resolves, Graphitron binds input-record components, service parameters, and Field<EpochSeconds> projections to Long everywhere EpochSeconds appears in the SDL.
Resolution order and failure modes
Graphitron’s scalar resolver tries three layers in order; first hit wins:
-
Spec built-ins.
Int,Float,String,Boolean,IDresolve through a closed table baked into the resolver.@scalarTypeon a spec built-in name is a hard validation error (directive conflict). -
@scalarType(scalar:)directive. The consumer’s explicit reference, takes precedence over the convention layer. -
graphql-java-extended-scalars convention. SDL name matches a convention-table entry and the artifact is on the compile classpath.
If none of the three layers produces a hit, the scalar is unresolved and the build fails with a validation error pointing at @scalarType(scalar:) or extended-scalars as the fix. There is no silent fallback to Object.
Field-level rejection reasons are surfaced with per-arm messages: FieldNotFound, FieldNotAccessible, NullAtCodegen, NotAScalarType, CoercingErased. The directive reference catalogues each one.
Migrate off manual additionalType(…) calls
Earlier rewrite snapshots taught registering scalars through the Graphitron.buildSchema(…) customizer with b.additionalType(ExtendedScalars.UUID). Once @scalarType resolves the same scalar, graphql-java’s GraphQLSchema.Builder.additionalType rejects the duplicate type-name at build time with a SchemaProblem. The migration is mechanical: remove the manual .additionalType(<scalarConstant>) line from your customizer for every scalar Graphitron now resolves.
The customizer is still the right place for non-scalar additional types and for custom directives; the additivity contract documented in the runtime API reference is unchanged.
Editor support
The graphitron LSP completes @scalarType(scalar: |) on a scalar X declaration with the convention-table FQNs, preferring the entry that matches the SDL name when one exists. Diagnostics surface malformed references (no dot) and unknown classes (FQN whose class part is not on the compile classpath scan) inline; field-level failures (FieldNotFound, CoercingErased, etc.) surface during the build.
Constraints
-
@scalarTypeis only valid onscalardeclarations. Applying it to a spec built-in (Int,Float,String,Boolean,ID) is a directive-conflict validation error: the GraphQL spec and graphql-java already bind these names. -
The SDL
scalar <Name>declaration is what makes the name resolvable in the SDL; the@scalarType(scalar:)directive (or the convention-table fallback) is what makes the reference resolvable in the built schema. The SDL name and the registeredGraphQLScalarTypeconstant’sname()must match exactly. -
The referenced class must be on the codegen classpath (consumer module’s
target/classes, any reactor sibling’starget/classes, or a declared dependency). Plugin-classpath visibility does not count. -
The referenced field must be
public static, non-null at code generation, and assignable tographql.schema.GraphQLScalarType. -
The constant’s
Coercing<I, O>must declare concrete type parameters; raw,Object-erased, or anonymous-class declarations whoseIresolves toObjectare rejected with aCoercingErasedreason. -
Federation-namespaced scalars (e.g.
_FieldSet) are wired automatically when the SDL declares an Apollo Federation@link; do not redeclare them with@scalarType. How-to: Federation key fields covers the federation directive surface.
See also
-
@scalarTypedirective reference is the canonical surface for the directive: SDL signature, parameters, resolution order, and the per-arm rejection reasons in full. -
Runtime API reference documents
Graphitron.buildSchema(…), including the customizer’s additive-only contract for non-scalar extension. -
How-to: Federation key fields covers federation entity declaration; federation-namespaced scalars are wired by the federation transform automatically.
-
How-to: Polymorphic types covers the supported
.codeRegistry(UnaryOperator)extension point for type resolvers on hand-rolled interfaces and unions. -
The graphql-java-extended-scalars project: graphql-java-extended-scalars for ready-made
GraphQLScalarTypeconstants coveringBigDecimal,DateTime,UUID, and similar.