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 and pointing the @scalarType(scalar:) directive at a public static final GraphQLScalarType constant on the classpath. @scalarType is the single explicit way to bind any non-spec, non-federation scalar. 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 |
Reuse a library constant
graphql-java-extended-scalars ships ready-made constants for most common shapes. Point @scalarType at the constant you want; the SDL name is yours to choose and need not match the constant’s own name (an aliasing declaration registers under the SDL name, borrowing the constant’s coercing).
scalar UUID @scalarType(scalar: "graphql.scalars.ExtendedScalars.UUID")
scalar BigDecimal @scalarType(scalar: "graphql.scalars.ExtendedScalars.GraphQLBigDecimal")
The same directive binds a consumer’s own constant just as well, so modeling BigDecimal as your own Money wrapper is a one-line change:
scalar BigDecimal @scalarType(scalar: "com.example.Scalars.MONEY")
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 recognises two implicit paths plus the directive:
-
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). -
Federation-namespaced scalars (e.g.
_FieldSet) are auto-wired when the SDL declares an Apollo Federation@link; do not redeclare them with@scalarType. -
@scalarType(scalar:)directive. The single explicit binding path for every other scalar.
A non-spec, non-federation scalar with no @scalarType is unresolved and the build fails with a validation error naming @scalarType(scalar:) 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 from the GraphQLScalarType constants scanned off the codegen classpath (your own and any library’s), preferring one whose field name matches the SDL scalar name when it 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 is what makes the reference resolvable in the built schema. When the SDL name matches the constant’s ownname()the constant is registered directly; when it differs, the scalar registers under the SDL name borrowing the constant’s coercing (an aliasing declaration). -
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.