Decorators
A decorator selects nodes in the schema — a type, field, or argument the printer is about to render — with a predicate (by default, every node), and renders values produced by a resolve callback (by default, none) with a render callback you provide. A decorator with a title becomes its own top-level section of the type page; one without renders bare content into a named slot instead, such as a badge next to the heading or a line appended to the description.
decorators replaces the deprecated customDirective option — it is the recommended way to render directive-driven (and now non-directive) content going forward (see migrating from customDirective below). There is no built-in "this decorator's own directive" shortcut — a directive-driven decorator opts in explicitly with predicate: hasDirectiveNamed("name"), as the walkthrough below shows.
Usage
This walkthrough shows the most common case, a decorator driven by a directive; a directive is not required — see response type for operations below for one driven entirely by the schema's own shape instead.
1. Declare a directive in the schema
Mark it repeatable when a type can carry more than one occurrence.
directive @httpResponse(
code: Int!
description: String
) repeatable on FIELD_DEFINITION
type Query {
user(id: ID!): User
@httpResponse(code: 200, description: "OK")
@httpResponse(code: 404, description: "User not found")
}
2. Declare the decorator in the configuration
const { getDirectiveFromSchema, getTypeDirectiveValuesList, hasDirectiveNamed } = require("@graphql-markdown/graphql");
decorators: {
responses: {
predicate: hasDirectiveNamed("httpResponse"),
title: "Responses",
position: { after: "metadata" },
resolve: (type, options) => {
const directive = getDirectiveFromSchema("httpResponse", options);
return directive ? getTypeDirectiveValuesList(directive, type) : [];
},
render: (values) => {
return [
"| Code | Description |",
"| ---- | ----------- |",
...values.map((value) => `| \`${value.code}\` | ${value.description ?? ""} |`),
].join("\n");
},
},
}
resolve here is exactly what directiveOccurrences does — use resolve: directiveOccurrences("httpResponse") instead of spelling it out, unless the values need further transformation.
3. The section is rendered on the page
### Responses
| Code | Description |
| ----- | -------------- |
| `200` | OK |
| `404` | User not found |
Options
The key is a free-form, unique id — it does not need to name a schema directive, and does not become the section heading (set title for that). It must not be one of the built-in section names (see Position).
| Option | Required | Description |
|---|---|---|
predicate | no | Selects the nodes this decorator applies to (see Predicate). Defaults to matching every node. |
resolve | no | Produces the values passed to render (see Resolve). Defaults to none (an empty array). |
render | yes | Callback returning the content as Markdown (see Render). |
title | no | Section heading. Omit for bare, titleless output — this is how a badge or an appended description line is expressed (see Position). |
level | no | Heading level, defaults to 3. Ignored when title is absent. |
position | no | Placement relative to another section, or into a named slot (see Position). Defaults to last. |
A decorator is skipped, and nothing is printed, when its predicate does not match, resolve returns nothing, or render returns nothing.
Predicate
predicate is (type, options) => boolean, evaluated once per node, and defaults to matching every node (always()). @graphql-markdown/graphql exports the common building blocks:
hasDirectiveNamed(name)— the node carries a directive namedname.hasAnyDirective()— the node carries at least one directive.isEntity(...kinds)— the node's schema entity kind is one ofkinds("queries","mutations","subscriptions","objects","interfaces","unions","enums","inputs","scalars","directives").and(...predicates),or(...predicates),not(predicate)— compose predicates.always()— matches every node; the default.
const { hasDirectiveNamed, isEntity, and } = require("@graphql-markdown/graphql");
decorators: {
responses: {
predicate: and(hasDirectiveNamed("httpResponse"), isEntity("queries", "mutations")),
title: "Responses",
resolve: /* ... */,
render: (values) => values.map((v) => `- \`${v.code}\` ${v.description}`).join("\n"),
},
}
A decorator declaring an explicit predicate but no resolve still renders once, with an empty record, whenever the predicate matches — this is a marker decorator (see the marker example below), useful for a directive whose mere presence is the content. If that is not the intent, supply resolve too.
Resolve
resolve is (type, options) => values, producing the records render receives; it defaults to producing none. A decorator declaring neither predicate nor resolve is a no-op by construction (predicate matches everything, resolve produces nothing to substitute) — declare at least one.
For a directive-driven decorator that needs the directive's argument values, @graphql-markdown/graphql provides a ready-made resolve:
const { directiveOccurrences, directiveOccurrence } = require("@graphql-markdown/graphql");
// one record per occurrence, in schema declaration order — for a `repeatable` directive
resolve: directiveOccurrences("httpResponse"),
// a single record — for a directive that is not `repeatable`
resolve: directiveOccurrence("meta"),
Write resolve by hand instead when the values need further transformation, or do not come from a directive's arguments at all — for instance, derived from a nested field, or from a different data source entirely:
resolve: (type, options) => {
const directive = getDirectiveFromSchema("httpResponse", options);
return directive ? getTypeDirectiveValuesList(directive, type) : [];
},
directiveOccurrences/directiveOccurrence are exactly this pattern, packaged up.
Render
render receives the resolved values, the print options in effect, and a context:
render: (values, options, context) => {
// values: [ { code: 200, description: "OK" }, { code: 404, description: "User not found" } ]
// options: the print options in effect for the node being rendered
// context: { id, type, entity }
};
context.id— the decorator's id.context.type— the GraphQL node being printed.context.entity— the node's schema entity kind, when resolvable.
There is no context.directive: a decorator that only needs a directive's definition (not per-occurrence argument values), such as one wrapping directiveDescriptor/directiveTag, can resolve it directly in render with getDirectiveFromSchema, skipping resolve entirely — or with @graphql-markdown/helpers's withDirective, which wraps exactly that lookup (see migrating from customDirective for a full example).
Optional directive arguments that were omitted are absent from a resolved record rather than set to undefined, so give them a fallback.
A decorator declared without a title renders bare content: this is how a badge or an appended description line is expressed, using position: { into: <slot> } to say where.
Position
position places a decorator relative to another one, or into a named slot outside the page's section order.
Splicing into the section order — { after: "<section>" } or { before: "<section>" }. The built-in sections are, in their default order:
tags, description, code, metadata, example, relations
Another decorator can also be named, by its id, as long as it is declared earlier. A decorator whose position names an unknown section is appended last.
Into a named slot — { into: "<slot>" } appends the decorator's bare (titleless) content into a slot that is not itself a page section:
| Slot | Appears |
|---|---|
description | Appended after the node's description text (the type's, or a field/argument's). |
tags | Alongside the type-badges/deprecation tags on the heading's metadata line. |
badges | Alongside the built-in type badges (non-null, scalar, …) on a member's metadata line. |
permalink | Next to the permalink icon on a member's metadata line. |
metadata | Appended at the end of a member's metadata line, after badges/tags/permalink. |
A decorator using into is excluded from the page's section order entirely — it never has a heading, regardless of title.
badges, permalink, and metadata are only reachable from a member's line (a field, argument, or enum value) — there is no type-heading equivalent, so a decorator targeting one of them never renders for the type itself; use description or tags for type-level placement instead. Conversely, description and tags are shared between the heading and every member row: a predicate that isn't scoped tightly enough (matching, say, both a type and its own fields) renders the decorator in both places.
example is itself a decorator, specialized: it is built from the printTypeOptions.exampleSection option and rendered as a code block. It is configured through that option, not through decorators.
Use beforeComposePageTypeHook when the placement has to be decided per type, rather than once in the configuration.
Examples
Response headers
directive @httpHeader(
name: String!
required: Boolean = false
) repeatable on FIELD_DEFINITION
const { directiveOccurrences, hasDirectiveNamed } = require("@graphql-markdown/graphql");
{
httpHeader: {
predicate: hasDirectiveNamed("httpHeader"),
title: "Headers",
position: { after: "metadata" },
resolve: directiveOccurrences("httpHeader"),
render: (values) => {
return values
.map((value) => `- \`${value.name}\`${value.required ? " *(required)*" : ""}`)
.join("\n");
},
},
}
A badge from a directive with no arguments
A decorator with an explicit predicate and no resolve still renders once when the directive is present with no arguments to carry (a marker decorator) — useful for a plain presence badge.
directive @beta on OBJECT | FIELD_DEFINITION
const { hasDirectiveNamed } = require("@graphql-markdown/graphql");
{
beta: {
predicate: hasDirectiveNamed("beta"),
position: { into: "tags" },
render: (values, options) => options.formatMDXBadge({ text: "BETA", classname: "badge--danger" }),
},
}
Meta object
A directive naming another documented type, rendered as a link to its page. @meta is not repeatable, so directiveOccurrence (singular) resolves its one occurrence.
directive @meta(type: String!) on FIELD_DEFINITION
const { directiveOccurrence, hasDirectiveNamed } = require("@graphql-markdown/graphql");
{
meta: {
predicate: hasDirectiveNamed("meta"),
title: "Meta",
position: { after: "code" },
resolve: directiveOccurrence("meta"),
render: ([value], options) => {
const slug = String(value.type).toLowerCase();
return `Returned alongside the data: [\`${value.type}\`](${options.basePath}/objects/${slug}).`;
},
},
}
Response type for operations
Not every decorator needs a directive at all: predicate and resolve can just as well select and derive content from the schema's own shape. This appends each query/mutation's return type as its own SDL code block, reusing Printer.printCode:
const { getNamedType, isOperation, isScalarType } = require("@graphql-markdown/graphql");
const { Printer } = require("@graphql-markdown/printer-legacy");
{
responseType: {
predicate: isOperation,
title: "Response Type",
position: { after: "code" },
resolve: (type, options) => {
const returnType = getNamedType(type.type);
if (isScalarType(returnType)) {
return [];
}
return [{ code: Printer.printCode(returnType, options) }];
},
render: ([value]) => value.code,
},
}
isOperation only takes type, one argument fewer than predicate's (type, options) — that's fine, since predicate is always called with both, and the extra one is simply ignored. Any single-argument type guard from @graphql-markdown/graphql (isObjectType, isEnumType, …) can be passed directly as predicate the same way.
This mirrors the afterPrintCode hook recipe, which achieves the same result by rewriting the generated code block directly. Prefer this decorator form when the content should be orderable via position or need not touch the code block itself; prefer the hook when you need to intercept printCode's raw output.
Migrating from customDirective
customDirective is deprecated in favor of decorators; both flow through the same rendering pipeline, but decorators selects nodes with any predicate, not only a directive's presence, and lets a decorator target any section position or slot rather than only a description line, a tag, or the built-in "Directives" section.
- customDirective: {
- auth: {
- descriptor: (directive, node) =>
- directiveDescriptor(directive, node, "Requires the `${requires}` role."),
- tag: (directive) => ({ text: `@${directive.name}` }),
- },
- },
+ decorators: {
+ authDescription: {
+ predicate: hasDirectiveNamed("auth"),
+ position: { into: "description" },
+ render: withDirective("auth", (directive, options, { type }) =>
+ directiveDescriptor(directive, type, "Requires the `${requires}` role."),
+ ),
+ },
+ authTag: {
+ predicate: hasDirectiveNamed("auth"),
+ position: { into: "tags" },
+ render: withDirective("auth", (directive, options) =>
+ options.formatMDXBadge({ text: `@${directive.name}` }),
+ ),
+ },
+ },
A customDirective entry's descriptor/tag each become their own decorator, both gated with predicate: hasDirectiveNamed(<same name>); descriptor targets the description slot, tag the tags slot. directiveDescriptor/directiveTag (from @graphql-markdown/helpers) still work unchanged — only the surrounding wiring changes: resolve is not needed here, since descriptor/tag operate on the directive definition, not per-occurrence argument values. withDirective (also from @graphql-markdown/helpers) is descriptor/tag's old implicit directive lookup, made explicit: it resolves the named directive and skips render entirely when the schema does not declare it, rather than every decorator repeating that getDirectiveFromSchema + null-check by hand. A badge decorator formats its own Markdown via options.formatMDXBadge, the same formatter the printer uses for its own badges.
Helpers
The packages @graphql-markdown/helpers and @graphql-markdown/graphql provide a few helper functions to quickly start.
@graphql-markdown/helpers is an optional peer dependency, and it needs to be installed before using it.
npm i @graphql-markdown/helpers
@graphql-markdown/helpers
directiveDescriptordirectiveTagwithDirective— wraps arenderthat only needs a directive's definition (see migrating fromcustomDirective).
@graphql-markdown/graphql
Predicate helpers (see Predicate):
hasDirectiveNamedhasAnyDirectiveisEntityand,or,notalwaysisOperationand the other type guards — any single-argument one doubles as a predicate (see response type for operations).
Directive-value helpers (see Resolve):
directiveOccurrences— ready-maderesolvefor arepeatabledirective.directiveOccurrence— ready-maderesolvefor a non-repeatable directive.getDirectiveFromSchemagetTypeDirectiveValuesgetTypeDirectiveValuesListgetTypeDirectiveArgValue
@graphql-markdown/printer-legacy
Printer.printCode— renders a type's SDL as a code block, reused in response type for operations.