Skip to main content
Version: 8.x.x

Directives

GraphQL directives can be used to transform the schema types, fields and arguments as well as modify the runtime behavior of the query (e.g. implement access control, etc). Common use cases involve limiting functionality based on the user authentication and authorization. While GraphQL spec specifies two types of directives - executable (aka query) and type system (aka schema) directives, only the latter one is supported by graphql-kotlin-schema-generator.

Default Directives

@deprecated - schema directive used to represent deprecated portion of the schema. See @Deprecated and @GraphQLDeprecated annotation documentation for more details

type Query {
deprecatedQuery: Boolean! @deprecated(reason: "No longer supported")
}

@skip - query directive that allows for conditional exclusion of fields or fragments

query myQuery($shouldSkip: Boolean) {
myField @skip(if: $shouldSkip)
}

@include - query directive that allows for conditional inclusion of fields or fragments

query myQuery($shouldInclude: Boolean) {
myField @include(if: $shouldInclude)
}

Custom Directives

Custom directives can be added to the schema using custom annotations:

@GraphQLDirective(
name = "awesome",
description = "This element is great",
locations = [FIELD_DEFINITION]
)
annotation class AwesomeDirective(val value: String)

class MyQuery {
@AwesomeDirective("cool stuff")
val somethingGreat: String = "Hello World"
}

The directive will then added to the schema as:

# This element is great
directive @awesome(value: String) on FIELD_DEFINITION

type MyQuery {
somethingGreat: String @awesome("cool stuff")
}

Directives can be added to various places in the schema. See the graphql.introspection.Introspection.DirectiveLocation enum from graphql-java for a full list of valid locations.

note

GraphQL directives are currently not available through introspection and you have to use SDL directly instead (you can use convenient print extension function of GraphQLSchema). See GraphQL issue and corresponding graphql-java issue for more details about the introspection issue.

Naming Convention

As described in the example above, the directive name in the schema will by default come from the @GraphQLDirective.name attribute which should follow lowerCamelCase format. If this value is not specified, the directive name will default to the normalized decapitalized name of the annotated annotation (eg: awesomeDirective in the example above).

Customizing Behavior

Directives allow you to customize the behavior of your schema based on some predefined conditions. Simplest way to modify the default behavior of your GraphQLTypes is by providing your custom KotlinSchemaDirectiveWiring through KotlinDirectiveWiringFactory factory used by your SchemaGeneratorHooks.

Example of a directive that converts field to lowercase

@GraphQLDirective(name = "lowercase", description = "Modifies the string field to lowercase")
annotation class LowercaseDirective

class LowercaseSchemaDirectiveWiring : KotlinSchemaDirectiveWiring {

override fun onField(environment: KotlinFieldDirectiveEnvironment): GraphQLFieldDefinition {
val field = environment.element
val originalDataFetcher: DataFetcher<Any> = environment.getDataFetcher()

val lowerCaseFetcher = DataFetcherFactories.wrapDataFetcher(
originalDataFetcher,
BiFunction<DataFetchingEnvironment, Any, Any>{ _, value -> value.toString().toLowerCase() }
)
environment.setDataFetcher(lowerCaseFetcher)
return field
}
}

While you can manually apply all the runtime wirings to the corresponding GraphQL types directly in SchemaGeneratorHooks#onRewireGraphQLType, we recommend the usage of our KotlinDirectiveWiringFactory to simplify the integrations. KotlinDirectiveWiringFactory accepts a mapping of directives to corresponding wirings or could be extended to provide the wirings through KotlinDirectiveWiringFactory#getSchemaDirectiveWiring that accepts KotlinSchemaDirectiveEnvironment.

val queries = ...
val customWiringFactory = KotlinDirectiveWiringFactory(
manualWiring = mapOf<String, KotlinSchemaDirectiveWiring>("lowercase" to LowercaseSchemaDirectiveWiring()))
val customHooks = object : SchemaGeneratorHooks {
override val wiringFactory: KotlinDirectiveWiringFactory
get() = customWiringFactory
}
val schemaGeneratorConfig = SchemaGeneratorConfig(hooks = customHooks)
val schema = toSchema(queries = queries, config = schemaGeneratorConfig)

While providing directives on different schema elements you will be able to modify the underlying GraphQL types. Keep in mind though that data fetchers are used to resolve the fields so only field directives (and by association their arguments directives) can modify runtime behavior based on the context and user input.

caution

graphql-kotlin prioritizes manual wiring mappings over the wirings provided by the KotlinDirectiveWiringFactory#getSchemaDirectiveWiring. This is a different behavior than graphql-java which will first attempt to use WiringFactory and then fallback to manual overrides.

For more details please refer to the example usage of directives in our example app.

Repeatable Directives

GraphQL supports repeatable directives (e.g. Apollo federation allows developers to specify multiple @key directives). By default, Kotlin does not allow applying same annotation multiple times. In order to make your directives repeatable, you need to annotate it with kotlin.annotation.Repeatable annotation.

@Repeatable
@GraphQLDirective(locations = [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE])
annotation class MyRepeatableDirective(val value: String)

Generates the above directive as

directive @myRepeatableDirective(value: String!) repeatable on OBJECT | INTERFACE

Directive Chaining

Directives are applied in the order annotations are declared on the given object. Given

@Directive1
@Directive2
fun doSomething(): String {
// does something
}

Directive1 will be applied first followed by the Directive2.

Ignoring Directive Arguments

Normally if you wanted to exclude a field or argument from the schema, you could use @GraphQLIgnore. However, due to reflection and kotlin limitations, the generated JVM code for interface arguments can only have annotations on getters.

This is easily fixable though using the @get: target prefix See graphql-kotlin#763 for more details.

@GraphQLDirective
annotation class DirectiveWithIgnoredArgs(
val string: String,

@get:GraphQLIgnore
val ignoreMe: String
)

This will generate the following schema

directive @directiveWithIgnoredArgs(
string: String!
) on ...

Limitations

GraphQL specification allows usage of any valid input objects as directive arguments. Since we rely on Kotlin annotation functionality to define our custom directives, we are limited in what can be used as annotation parameter - only primitives (or scalars), Strings, Enums, other annotations or an array of any of the above are supported.

Support for input objects can be added by providing that object representation as an annotation class and then adding support for it through custom schema generator hooks.