GraphQL Directives¶
GraphQL directives in django-graphex allow you to transform field values at query execution time. They provide a powerful way to format, manipulate, and transform data without modifying your underlying models or resolvers.
Overview¶
Directives are applied to fields in your GraphQL queries and are processed after the field value is resolved. They enable you to:
- :material-format-text: Format strings: Transform text case, encoding, and structure
- :material-calendar: Format dates: Display dates in various formats and relative time
- :material-calculator: Format numbers: Apply number formatting and currency display
- :material-shuffle-variant: Manipulate lists: Transform and sample list data
Usage¶
Directives are applied in GraphQL queries using the @directive_name syntax:
query {
user {
name @uppercase
email @lowercase
joinDate @date(format: "MMMM DD, YYYY")
balance @currency(symbol: "€")
}
}
String Directives¶
String directives provide various text transformation capabilities:
Case Conversion¶
Transform text case with these directives:
String Manipulation¶
query GetPost {
post {
# Cut on a word boundary, append "…" (default)
summary @truncate(length: 80) # "A long summary…"
# Cut mid-word with a custom suffix
teaser @truncate(length: 10, killwords: true, end: "...")
# URL-safe slug (Django slugify)
title @slugify # "My Post!" → "my-post"
}
}
| Directive | Args | Behavior |
|---|---|---|
@truncate |
length: Int!, end: String = "…", killwords: Boolean = false |
Shorten to length, appending end; break on a word boundary unless killwords. |
@slugify |
— | Convert to a URL-safe slug. |
Default Values¶
Provide fallback values for empty or null fields:
Encoding Directives¶
Handle base64 encoding and decoding:
Number Directives¶
Format numeric values with precision and style:
Basic Number Formatting¶
Currency Formatting¶
Format numbers as currency with customizable symbols:
Date Directives¶
Powerful date and time formatting with multiple options:
Standard Date Formats¶
query GetPost {
post {
createdAt @date(format: "YYYY-MM-DD") # "2023-12-01"
updatedAt @date(format: "MMMM DD, YYYY") # "December 01, 2023"
publishedAt @date(format: "DD/MM/YYYY HH:mm") # "01/12/2023 14:30"
timestamp @date(format: "iso") # "2023-Dec-01T14:30:00"
jsDate @date(format: "javascript") # "Fri Dec 01 2023 14:30:00"
}
}
Relative Time Formatting¶
Custom Date Patterns¶
Build custom date formats using these tokens:
| Token | Description | Example |
|---|---|---|
YYYY |
4-digit year | 2023 |
YY |
2-digit year | 23 |
MMMM |
Full month name | December |
MMM |
Short month name | Dec |
MM |
Month number (padded) | 12 |
DD |
Day of month (padded) | 01 |
dddd |
Full day name | Friday |
ddd |
Short day name | Fri |
HH |
Hour (24h, padded) | 14 |
hh |
Hour (12h, padded) | 02 |
mm |
Minutes (padded) | 30 |
ss |
Seconds (padded) | 45 |
A |
AM/PM | PM |
List Directives¶
Transform and manipulate list data:
Shuffle Directive¶
Randomly reorder list elements:
Sample Directive¶
Get a random sample from a list:
Unique Directive¶
De-duplicate a list while preserving order:
Math Directives¶
Perform mathematical operations on numbers:
Floor, Ceil, Round and Abs¶
String / Float fields and null
@floor, @ceil, @round and @abs work on both Float and String
fields — when the field is a String the result comes back as a string. A
null value passes through unchanged.
Combining Directives¶
Chain multiple directives for complex transformations:
query GetFormattedData {
user {
firstName @default(to: "Anonymous") @title_case @strip
email @lowercase @strip
bio @default(to: "No bio") @capitalize @replace(old: ".", new: "!")
}
post {
title @title_case @replace(old: "GraphQL", new: "GQL")
viewCount @number(as: ",.0f")
createdAt @date(format: "MMMM DD, YYYY")
}
}
Custom Directives¶
While django-graphex provides many built-in directives, you can create
your own. A custom directive is a class that transforms the resolved value of
a field. The full recipe is four steps, shown end-to-end below.
The example here is verified by the test suite (
tests/test_directives.py::CustomDirectiveTest), so it stays in sync with the code.
1. Define the directive¶
Subclass BaseExtraGraphQLDirective, declare any arguments in get_args(), and
transform the value in resolve(). The directive name is derived from the
class name (the GraphQLDirective suffix is stripped and the rest is
snake_cased), so MaskGraphQLDirective becomes @mask (and, e.g.,
CreditCardGraphQLDirective would become @credit_card).
# myapp/directives.py
from django_graphex.directives.base import BaseExtraGraphQLDirective
from graphql import GraphQLArgument, GraphQLNonNull, GraphQLInt, GraphQLString
class MaskGraphQLDirective(BaseExtraGraphQLDirective):
"""Keep the last `visible` characters, masking the rest."""
@staticmethod
def get_args():
return {
"visible": GraphQLArgument(
GraphQLNonNull(GraphQLInt),
description="Number of trailing characters to leave visible",
),
"char": GraphQLArgument(
GraphQLString, description="Masking character (default: '*')"
),
}
@staticmethod
def resolve(value, args, directive, root, info, **kwargs):
if not value:
return value
text = str(value)
visible = args.get("visible") or 0
char = args.get("char") or "*"
if visible >= len(text):
return text
return char * (len(text) - visible) + text[len(text) - visible:]
The resolve(value, args, directive, root, info, **kwargs) signature receives the
already-resolved field value and the coerced args dict — read them with
args.get("name"), no AST parsing required.
2. Register it on the schema¶
Pass an instance alongside all_directives (which already bundles the
built-ins plus the standard @skip / @include / @deprecated):
# myapp/schema.py
import graphene
from django_graphex import all_directives
from myapp.directives import MaskGraphQLDirective
schema = graphene.Schema(
query=Query,
directives=[*all_directives, MaskGraphQLDirective()],
)
django_graphex.ExtraGraphQLSchema accepts the same directives=
argument, so the snippet works with either schema class.
3. Enable the middleware¶
Custom directives are applied by ExtraGraphQLDirectiveMiddleware. Without it the
directive parses and validates but does nothing:
# settings.py
GRAPHENE = {
"SCHEMA": "myapp.schema.schema",
"MIDDLEWARE": ["django_graphex.ExtraGraphQLDirectiveMiddleware"],
}
4. Use it¶
query {
card @mask(visible: 4) # "4111111111111234" -> "************1234"
card @mask(visible: 4, char: "#") # -> "############1234"
}
Because the middleware coerces arguments with get_directive_values, every
directive argument may also be supplied as a GraphQL variable:
How it works (and two gotchas)
- These are execution / value-transform directives. They run on
FIELD,FRAGMENT_SPREADandINLINE_FRAGMENTlocations and post-process whatever the resolver returned — they are not schema/SDL type-system directives and do not change resolution logic. - Both steps are required: the directive must be in the schema's
directives=list and the middleware must be enabled. Instantiating the directive also self-registers it so the middleware can find it by name.
Schema Integration¶
Add directives to your GraphQL schema:
from django_graphex import all_directives
from .directives import MaskGraphQLDirective
# all_directives already includes the built-in @skip / @include /
# @deprecated directives, so just append your own.
custom_directives = [
*all_directives,
MaskGraphQLDirective()
]
schema = graphene.Schema(
query=Query,
directives=custom_directives
)
Middleware Integration¶
Enable directive processing with middleware:
Real-World Examples¶
Blog Post Formatting¶
query GetBlogPost {
post(id: "1") {
title @title_case
content @strip
excerpt @default(to: "No excerpt available") @capitalize
author {
name @title_case
email @lowercase
bio @default(to: "No bio") @strip
}
publishedAt @date(format: "MMMM DD, YYYY")
updatedAt @date(format: "time ago")
viewCount @number(as: ",.0f")
tags @sample(k: 5) {
name @uppercase
}
}
}
E-commerce Product Display¶
query GetProduct {
product(id: "123") {
name @title_case
description @strip @default(to: "No description available")
price @currency(symbol: "$")
originalPrice @currency(symbol: "$")
discount @number(as: ".0%")
weight @number(as: ".2f")
dimensions @replace(old: "x", new: " × ")
createdAt @date(format: "YYYY-MM-DD")
lastModified @date(format: "time ago")
reviews @shuffle {
rating @number(as: ".1f")
comment @strip @default(to: "No comment")
createdAt @date(format: "MMM DD, YYYY")
}
}
}
User Profile Display¶
query GetUserProfile {
user {
username @lowercase
displayName @default(to: "Anonymous User") @title_case
email @lowercase
bio @default(to: "No bio available") @strip @capitalize
location @title_case
website @lowercase
socialLinks {
twitter @replace(old: "https://twitter.com/", new: "@")
linkedin @lowercase
}
joinDate @date(format: "MMMM YYYY")
lastActive @date(format: "time ago")
postCount @number(as: ",.0f")
followerCount @number(as: ",.0f")
}
}
Performance Considerations¶
Performance Tips
- Directive Order: Directives are processed in order, so place expensive operations last
- Caching: Directive results aren't cached by default - consider caching formatted values
- Complex Formatting: For heavy date/time operations, consider pre-formatting in resolvers
- List Operations: Be cautious with shuffle/sample on very large lists
Error Handling¶
Directives handle errors gracefully:
Best Practices¶
Directive Best Practices
- Use Defaults: Always provide fallback values for nullable fields
- Format Consistently: Use the same date/number formats across your app
- Chain Wisely: Order directive chains logically (clean → transform → format)
- Test Edge Cases: Test with null, empty, and invalid values
- Document Usage: Document custom directive usage in your API documentation
- Consider Performance: Use directives for display formatting, not heavy processing
GraphQL directives in django-graphex provide a powerful, flexible way to format and transform your API responses, making your GraphQL API more user-friendly and consistent across different client applications.