Skip to content

Schemas

In Viaduct, a schema describes the GraphQL surface that a request can access. In the Star Wars demo, we register two schema IDs — one public and one with extra fields — and select between them at runtime based on scopes.

What a schema is

  • A set of SDL files (*.graphqls) under src/main/viaduct/schema.
  • A schema ID that names a concrete slice of the overall SDL (for example, public vs. public-with-extras).
  • A set of scope IDs, declared in src/main/viaduct/scopes.yaml, that controls which fields each slice contains.

The Star Wars demo declares two schema IDs:

version: 1

schemaScopes:
  - default
  - extras

scopedSchemas:
  - id: publicSchema
    scopes: [default]
  - id: publicSchemaWithExtras
    scopes: [default, extras]

View full file on GitHub

  • publicSchema — the default public surface.
  • publicSchemaWithExtras — includes everything from publicSchema plus extra fields marked with @scope(to: ["extras"]).

The build checks every @scope name against schemaScopes and builds both scoped schemas. See Scope Schemas for the file's syntax.

Where schemas are registered

Schemas are registered when creating a Viaduct instance. The demo reads the scoped schemas the build declared with SchemaScopeInfo.fromResources() and registers all of them (excerpt from ViaductConfiguration.kt):

ViaductBuilder()
    .withTenantModuleInjectorFactory(tenantModuleInjectorFactory)
    .withScopedSchemas(SchemaScopeInfo.fromResources())
    .withFlagManager(MAT_RESOLUTION_FLAG_MANAGER)
    .build()

View full file on GitHub

return new ViaductBuilder()
    .withTenantModuleInjectorFactory(tenantModuleInjectorFactory)
    .withScopedSchemas(SchemaScopeInfo.fromResources())
    .withFlagManager(
        flag ->
            flag == FlagManager.Flags.ENGINE2_ENABLED
                || FlagManager.Default.INSTANCE.isEnabled(flag))
    .build();

View full file on GitHub

  • withScopedSchemas registers exactly the listed views. Each declared scoped schema becomes a SchemaScopeInfo.Scoped; add SchemaScopeInfo.Base to also serve the unfiltered base view. Schemas without @scope need neither scopes.yaml nor withScopedSchemas.

Organizing SDL files

Place each tenant's GraphQL SDL files under its src/main/viaduct/schema directory. Keep entities modular (for example, character.graphqls, film.graphqls, species.graphqls) and use directives like @scope, @idOf, and @oneOf where appropriate.

Example (fragment)

type Planet implements Node @scope(to: ["default"]) @resolver(isBatching: true) {
  """
  The ID of an object
  """
  id: ID!

  """
  The name of this planet.
  """
  name: String

  """
  The diameter of this planet in kilometers.
  """
  diameter: Int

  """
  The number of standard hours it takes for this planet to complete a single
  rotation on its axis.
  """
  rotationPeriod: Int

  """
  The number of standard days it takes for this planet to complete a single orbit
  of its local star.
  """
  orbitalPeriod: Int

  """
  A number denoting the gravity of this planet, where "1" is normal or 1 standard
  G. "2" is twice or 2 standard Gs. "0.5" is half or 0.5 standard Gs.
  """
  gravity: Float

  """
  The average population of sentient beings inhabiting this planet.
  """
  population: Float

  """
  The percentage of the planet surface that is naturally occurring water or bodies
  of water.
  """
  surfaceWater: Float

  """
  The ISO 8601 date format of the time that this resource was created.
  """
  created: String

  """
  The ISO 8601 date format of the time that this resource was edited.
  """
  edited: String

  """
  The residents of this planet.
  """
  terrains: [String]

  """
  The films that this planet has appeared in.
  """
  climates: [String]

  """
  The residents of this planet.
  """
  residents(limit: Int): [Character!] @resolver

  """
  The films that this planet has appeared in.
  """
  films(limit: Int): [Film!] @resolver

View full file on GitHub

Fields like film are typically resolved by field/batch resolvers, not embedded in SDL logic.

Evolving the schema

To add a field that should exist only in the extras slice:

  1. Declare it in SDL with a scope:
extend type Species @scope(to: ["extras"]) {
  culturalNotes: String @resolver
  rarityLevel: String @resolver
  specialAbilities: [String] @resolver
  technologicalLevel: String @resolver
}

View full file on GitHub

  1. Ensure a scoped schema that includes extras is declared in scopes.yaml (publicSchemaWithExtras).
  2. Serve that schema ID on a route in the controller, as the demo does with /extras/graphql.

To add a new entity:

  • Define the type and relationships in SDL.
  • Implement node/field (and batch if needed) resolvers.
  • Emit typed IDs with ctx.globalIDFor(Type.Reflection, internalId) in Kotlin or context.globalIDFor(Type.ofClass(Character.class), internalId) in Java.
  • Write integration tests that hit both schema IDs if visibility differs.

Testing and troubleshooting

  • Validate that the expected fields appear in introspection under each schema ID.
  • Run end-to-end queries for both public and extras slices.
  • Confirm that resolvers are wired and batched correctly when many parents are selected.
  • Log the route and schemaId each request runs against.

Do and don’t

  • Do register schema IDs with clear scope sets (public vs. public + extras).
  • Do keep SDL modular and use directives for visibility and type-safety.
  • Don’t rely on a single “mega schema” and conditional logic inside resolvers to hide fields.

For multi-tenant schema management at the service layer, see the Multi-tenancy service engineer docs. For the full @scope directive reference, see the Scopes developer reference.