Skip to content

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 resolution
  • isBatching: Boolean! = false — when true, codegen generates a batchResolve method instead of resolve, 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:

type User {
  profileCacheKey: String! @tenantLocal
}

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 BackingData type. 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-@parent field 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 implement Node)

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:

interface Node @scope(to: ["*"]) {
  id: ID!
}

When it's included:

  • Your schema implements types that extend Node
  • You use the @idOf directive 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:

extend type Query @scope(to: ["*"]) {
  node(id: ID!): Node
  nodes(ids: [ID!]!): [Node]!
}

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? for JSON, or Any for JSON!
  • 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 or Map<String, Object> in Java.
  • JSON arrays become List<Any?> in Kotlin or List<Object> in Java.
  • JSON strings, booleans, and null become String, Boolean, and null.
  • 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 or Map<String, Object> in Java for objects, with string keys
  • List<Any?> in Kotlin or List<Object> in Java for arrays
  • String, Boolean, Number, or null for 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:

extend type Query {
  user(id: ID!): User @resolver
  users(limit: Int = 10): [User!]! @resolver
}

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 type for all root types — Never define Query or Mutation directly
  • Apply @resolver to fields fetching external data — This is how Viaduct knows which fields need custom logic
  • Use @idOf for type-safe IDs — Leverage compile-time validation for ID references
  • Apply @scope explicitly to sensitive fields — Don't rely on implicit visibility
  • Leverage built-in scalars — Use DateTime, Long, BigDecimal instead 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 @idOf are 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, not type Query

See Also