Schema Reference
Viaduct automatically provides a rich set of built-in schema components that are available in every application. This reference documents the directives, types, scalars, and other schema elements provided by the framework.
Built-in Directives¶
Viaduct includes core directives that are fundamental to the framework's functionality. These directives are automatically available and cannot be overridden.
@resolver¶
Marks fields or types that require custom resolution logic. This is the primary mechanism for implementing data fetching in Viaduct.
Locations: FIELD_DEFINITION, OBJECT
Arguments:
isSelective: Boolean! = false— enables selective resolutionisBatching: Boolean! = false— whentrue, codegen generates abatchResolvemethod instead ofresolve, enabling batch resolution for the field or type
Example:
type Query {
user(id: ID!): User @resolver
}
type User @resolver {
id: ID!
name: String
email: String
friends: [User] @resolver(isBatching: true)
}
Use cases:
- Fields that fetch data from external services or databases
- Fields that require custom business logic beyond simple property access
- Object types that need node resolution for Global ID support
- Fields or types that benefit from batch loading (
isBatching: true)
When you apply @resolver to a field, Viaduct generates an abstract resolver class that you must implement. The generated class contains either a resolve method (default) or a batchResolve method (when isBatching: true) — never both. At startup, Viaduct validates that the isBatching flag in the schema matches the method your resolver implements. See Resolvers and Batch Resolution for details.
@backingData¶
Specifies the backing data class for a field, enabling type-safe data access in resolvers. A backing-data field is an implementation detail: Viaduct keeps it available for use in required selection sets only in the module that defines the field. The field is not available in other modules or in any externally published schemas.
Locations: FIELD_DEFINITION
Arguments:
class: String!— fully qualified name of the backing data class
Example:
type User {
profileData: BackingData
@backingData(class: "com.example.data.UserProfileData")
@resolver
}
The field's base type must be BackingData, and a field whose base type is BackingData must carry @backingData. Backing-data fields can only be declared directly on object types; they cannot be declared on interfaces, inherited from interfaces, or used as input fields.
See the @backingData guide for a complete resolver example.
@tenantLocal¶
Marks an internal field that is available to code in the field's owning tenant but is not part of a client-executable schema. This directive is only applicable to fields whose base type is a scalar (for example, Int) or BackingData. BackingData fields and @parent fields are also tenant-local automatically; they do not need this directive.
Locations: FIELD_DEFINITION
Example:
Viaduct keeps tenant-local fields in the internal full schema so generated code, resolver required selection sets, and execution internals can use them. It removes them from Base and Scoped executable schemas, so clients cannot query them.
An explicit @tenantLocal field:
- Must return a scalar or
BackingDatatype. Lists and non-null wrappers are allowed. - Must be declared directly on an object type.
- Cannot be declared on an interface or implement a field inherited from an interface.
Fields with the @parent directive and fields whose base type is BackingData have the same schema-visibility behavior automatically. Do not add @tenantLocal merely to make those fields private.
@parent¶
Marks a field that resolves to the object from which the current object was reached during execution. The engine resolves the field from execution ancestry; it does not invoke a field resolver for it.
Locations: FIELD_DEFINITION
Example:
type Company {
name: String!
user: User @resolver
}
type User {
parent: Company @parent
companyDisplayName: String @resolver
}
A resolver or checker for a field on User can include parent { name } in its required selection set. See Parent fields in required selection sets.
Parent fields:
- Are tenant-local automatically and are removed from Base and Scoped executable schemas.
- Must be declared directly on an object type, not on an interface or an inherited interface field.
- May be nullable or non-null and return an object, interface, or union type, but cannot return a list.
- Cannot take arguments or carry
@resolver; the engine resolves the field from execution ancestry. - Require exactly one non-
@parentfield in the schema whose unwrapped return type is the child type. That producer field's containing type must be compatible with the declared parent return type. - Cannot target a type with a selective type resolver or a parent value whose nearest upstream field resolver is selective.
The @resolver restriction applies to the @parent field, not to the field that produces the child. A producer field may carry @resolver and may return the child directly, in a list, or in a nested list. A non-selective resolver that produces the parent value is also allowed; it prevents a selective resolver farther upstream from affecting this validation.
@namespaceType¶
Groups related fields under a dedicated type that acts as an organizational namespace on the root query type. The engine auto-resolves fields that return a namespace type — no resolver is needed for the namespace field itself.
Locations: OBJECT
Example:
type Query {
listings: Listings
}
type Listings @namespaceType {
availableRoomTypes: [RoomType] @resolver
pricing: ListingsPricing
}
type ListingsPricing @namespaceType {
currencyOptions: [Currency] @resolver
}
See Namespace Types for detailed documentation.
@scope¶
Controls field and type visibility across different schema scopes. This is a repeatable directive that enables multi-tenant or multi-variant schemas.
Locations: OBJECT, INTERFACE, UNION, ENUM, INPUT_OBJECT, FIELD_DEFINITION, ENUM_VALUE
Arguments:
to: [String!]!— list of scope names where this element is visible
Example:
type User @scope(to: ["public"]) {
id: ID!
name: String!
email: String @scope(to: ["internal"])
adminNotes: String @scope(to: ["admin"])
}
type InternalMetrics @scope(to: ["internal"]) {
requestCount: Long!
errorRate: Float!
}
See Scopes for detailed documentation on using scopes.
@idOf¶
Declares that an ID field or argument represents a Global ID for a specific GraphQL type. When a field or argument has @idOf, Viaduct generates code using GlobalID<T> instead of String in the resolver signature. This enables type-safe ID handling.
Locations: FIELD_DEFINITION, INPUT_FIELD_DEFINITION, ARGUMENT_DEFINITION
Arguments:
type: String!— name of the GraphQL type this ID references (must implementNode)
Example:
type Query {
user(id: ID! @idOf(type: "User")): User @resolver
users(ids: [ID!]! @idOf(type: "User")): [User!]! @resolver
}
input UpdateUserInput {
userId: ID! @idOf(type: "User")
name: String
}
See Global IDs for more information.
Built-in Types¶
Node Interface¶
The standard GraphQL Relay Node interface for entity identification. Viaduct automatically includes this interface when it's used in your schema.
Definition:
When it's included:
- Your schema implements types that extend
Node - You use the
@idOfdirective anywhere in your schema
Example usage:
type User implements Node {
id: ID!
name: String!
}
type Post implements Node {
id: ID!
title: String!
author: User @resolver
}
Node Query Fields¶
Viaduct automatically provides these root query fields when your schema uses the Node interface:
Query.node and Query.nodes come with built-in resolvers that work with Viaduct's Global ID system: based on the type embedded in a GlobalID, they will automatically call that type's node-resolver to obtain their results.
Built-in Scalars¶
Viaduct provides extended scalar types beyond GraphQL's standard scalars (Int, Float, String, Boolean, ID). These are automatically available without explicit declaration.
Date¶
ISO 8601 date format (YYYY-MM-DD).
Example value: "2024-10-29"
Kotlin type mapping: java.time.LocalDate
DateTime¶
ISO 8601 date-time format with timezone.
Example value: "2024-10-29T14:30:00Z"
Kotlin type mapping: java.time.Instant
Long¶
64-bit signed integer, beyond GraphQL's standard Int (32-bit).
Example value: 9223372036854775807
Kotlin type mapping: Long
BigDecimal¶
Arbitrary precision decimal number.
Example value: "123.456789012345"
Kotlin type mapping: java.math.BigDecimal
BigInteger¶
Arbitrary precision integer.
Example value: "12345678901234567890"
Kotlin type mapping: java.math.BigInteger
JSON¶
Represents any JSON value, including objects, arrays, and scalar values.
Example value: {"key": "value", "nested": {"count": 42}}
Generated type mappings:
- Kotlin:
Any?forJSON, orAnyforJSON! - Java:
Object; nullability follows the GraphQL field or argument declaration
These language-specific types expose the same recursive runtime JSON value model.
Resolver inputs¶
For a JSON argument or input field, resolvers receive a recursively decoded JSON value:
- JSON objects become
Map<String, Any?>in Kotlin orMap<String, Object>in Java. - JSON arrays become
List<Any?>in Kotlin orList<Object>in Java. - JSON strings, booleans, and null become
String,Boolean, andnull. - JSON numbers become a subtype of
java.lang.Number.
Do not depend on a particular numeric subtype. The concrete type can differ based on whether the value came from a literal or variable and on conversions performed while passing the value to the resolver.
Values nested inside JSON do not use Viaduct's other scalar coercions. For example, dates and
timestamps are strings, not LocalDate or Instant.
Resolver outputs¶
Resolvers should produce a recursively JSON-compatible value:
Map<String, Any?>in Kotlin orMap<String, Object>in Java for objects, with string keysList<Any?>in Kotlin orList<Object>in Java for arraysString,Boolean,Number, ornullfor scalar values
Standard JVM numeric types, including Integer, Long, Float, Double, BigInteger, and
BigDecimal, represent JSON numbers; BigDecimal is not required. This includes Kotlin Int
and Java int, which are boxed as java.lang.Integer when used as an Any or Object value.
Viaduct does not call toString() on other object types to turn them into JSON strings. The
embedding application's JSON serializer may serialize such values as objects or reject them, so
resolvers should explicitly convert arbitrary objects, dates, and timestamps to the
JSON-compatible representation they intend to expose.
BackingData¶
Internal marker type used by fields carrying the @backingData directive. A backing-data field's base type must be BackingData; Viaduct maps its value to the class named by the directive.
Root Types¶
Viaduct intelligently manages root types based on your schema definitions:
Query¶
Always created. Required by the GraphQL specification.
You must use extend type Query in your schema files:
Mutation¶
Created only when mutation extensions exist. Viaduct automatically creates the Mutation root type when it detects extend type Mutation in your schema.
extend type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload @resolver
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload @resolver
}
Directive Summary¶
| Directive | Locations | Purpose | Generated Code Impact |
|---|---|---|---|
@resolver(isSelective: Boolean! = false, isBatching: Boolean! = false) |
FIELD_DEFINITION, OBJECT | Marks fields/types requiring custom resolution | Generates resolve (default) or batchResolve (isBatching: true) |
@backingData(class: String!) |
FIELD_DEFINITION | Binds a tenant-local BackingData field to a JVM class |
Generates typed access to the backing data |
@tenantLocal |
FIELD_DEFINITION | Hides an internal scalar field from executable schemas | Keeps the field available to tenant code |
@parent |
FIELD_DEFINITION | Resolves the current object's execution parent | Generates typed access to selected parent fields |
@namespaceType |
OBJECT | Groups related fields under an organizational namespace | Engine auto-resolves the parent field |
@scope(to: [String!]!) |
OBJECT, INTERFACE, UNION, ENUM, INPUT_OBJECT, FIELD_DEFINITION, ENUM_VALUE | Controls visibility by scope (repeatable) | Affects schema filtering |
@idOf(type: String!) |
FIELD_DEFINITION, INPUT_FIELD_DEFINITION, ARGUMENT_DEFINITION | Declares Global ID type | Uses GlobalID<T> instead of String |
Scalar Summary¶
| Scalar | Description | Kotlin Type | Example Value |
|---|---|---|---|
| Date | ISO 8601 date | java.time.LocalDate |
"2024-10-29" |
| DateTime | ISO 8601 date-time | java.time.Instant |
"2024-10-29T14:30:00Z" |
| Long | 64-bit integer | Long |
9223372036854775807 |
| BigDecimal | Arbitrary precision decimal | java.math.BigDecimal |
"123.456789" |
| BigInteger | Arbitrary precision integer | java.math.BigInteger |
"12345678901234567890" |
| JSON | Generic JSON value | Any? |
{"key": "value"} |
| Upload | File upload | Implementation-specific | (binary) |
| BackingData | Internal backing data ref | Internal | (internal) |
Best Practices¶
Do¶
- Use
extend typefor all root types — Never defineQueryorMutationdirectly - Apply
@resolverto fields fetching external data — This is how Viaduct knows which fields need custom logic - Use
@idOffor type-safe IDs — Leverage compile-time validation for ID references - Apply
@scopeexplicitly to sensitive fields — Don't rely on implicit visibility - Leverage built-in scalars — Use
DateTime,Long,BigDecimalinstead of strings or custom scalars - Implement Node interface for entities — Use for globally identifiable objects
Don't¶
- Don't override core directives — Framework directives such as
@resolver,@backingData,@tenantLocal,@parent,@scope, and@idOfare provided automatically - Don't redefine standard scalars — They're automatically available
- Don't manually add the Node interface — It's added automatically when used
- Don't forget to extend root types — Always use
extend type Query, nottype Query
See Also¶
- Resolvers — Implementing resolvers for fields marked with
@resolver - Global IDs — Working with
@idOfand the Node interface - Namespace Types — Organizing root fields with
@namespaceType - Scopes — Advanced scope configuration with
@scope - Service Engineers: Schema Extensions — Defining application-wide custom directives and types
- Star Wars: Custom Directives — Examples from the Star Wars demo