- name
- client-generators
- description
- Write or change a Redocly client generator — the API model, the language-neutral helper toolkit, and the edit → regenerate → diff loop.
# Writing custom client generators
A generator is a plain module: `(input) => GeneratedFile[]`. It receives the
language-agnostic API model and returns files — in ANY output language. It runs
in the same pass as the built-ins; select it by path in `redocly.yaml`:
```yaml
client:
generators: [typescript, ./generators/my-generator.mjs]
```
## The contract
```js
/** @type {import('@redocly/client-generator').CustomGenerator} */
export default {
name: 'my-generator',
run({ model, output, outputMode, emit }) {
return [{ path: output.path.replace(/\.ts$/, '.mine.txt'), content: '…' }];
},
// Optional: one idiomatic call snippet per operation for docs (x-codeSamples),
// collected into an overlay file when `client.codeSamples: true` is set.
sample(operation, { model, emit }) {
return { lang: 'python', source: '…' };
},
// Optional: the reference page for what `run` emits, written when `client.docs` (or
// --docs) is on. Same `{ path, content }` shape as `run`; `renderReferencePage` gives
// the standard layout and takes `sample` for its snippets. A generator documents itself.
docs({ model, output, emit }) {
return [{ path: output.path.replace(/\.ts$/, '.mine.md'), content: '…' }];
},
};
```
## Declaring options
A generator that needs configuration declares it as a schema; `run` then receives
`options` already validated, with defaults applied:
```js
export default {
name: 'permissions-matrix',
options: {
type: 'object',
properties: { groupBy: { enum: ['tag', 'path'], default: 'tag' } },
additionalProperties: false,
},
run({ model, output, options }) {
return [
{ path: output.path.replace(/\.ts$/, '.permissions.md'), content: render(options.groupBy) },
];
},
};
```
Users set them per generator name:
```yaml
client:
generators: [typescript, ./generators/permissions-matrix.mjs]
options:
permissions-matrix:
groupBy: path
```
The supported subset is a top-level `type: 'object'` with `properties`, `required`, and
`additionalProperties`; each property is a scalar (`string`/`number`/`boolean`), an
`enum`, or an array of scalars, and may carry a `default` and a `description`. Don't
validate options inside `run` — an unknown key, a wrong type, a value outside an `enum`,
or a missing `required` key already fails generation before `run` is called.
Rules: output is deterministic (same description → same bytes); never add
dependencies to the generated client; **never hand-edit generated output** —
edit this generator and regenerate. Emitted file paths must stay inside the
`--output` directory (subdirectories are fine) — escapes are rejected.
Optionally declare `requiresGenerator` — the `@redocly/client-generator` version
range you wrote this against (`'^1.2.0'`, `'~1.2.0'`, `'>=1.2.0'`, or an exact
version). A CLI outside the range then fails with the fix path instead of feeding
your generator an unexpected model shape. Ejected generators carry it
automatically; hand-written ones without it are taken as current.
## The model (IR)
`model.services[].operations[]` — each operation carries `name`, `specName`,
`method`, `path`, `tags`, `pathParams`/`queryParams`/`headerParams`/`cookieParams`,
`requestBody`, `successResponses`/`errorResponses` (each with a `schema`), and
`security`. `model.schemas` holds the named schemas. Every schema is a
discriminated union on `kind`: `scalar`, `array`, `object`, `record`, `ref`,
`literal`, `enum`, `union` (optionally with a discriminator), `intersection`
(allOf), `null`, `unknown`, `omit`.
## Helpers (import from '@redocly/client-generator')
| Helper | Use |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `flattenAllOf(schema, model)` | The merged property view of allOf compositions — languages without intersection types render this. |
| `deref(schema, model)` | Follow a `ref` chain to the schema it names (cycle-guarded). |
| `jsonSuccessSchema(op)` / `sseResponse(op)` | The primary JSON success schema; the `text/event-stream` response when the operation streams. |
| `isMultipartBody(op)` | Whether the request body is multipart. |
| `isUntypedMultipartBody(op)` | Whether the request body is multipart with a schema that is neither an object nor a `$ref`, so the caller supplies the encoded body. |
| `isBinaryContentType(contentType)` | Whether a body of this content type is bytes (archives, PDF, images, media) — decode it as a Blob, not JSON or text. |
| `serverUrlParts(server)` | A server-URL template as literal/variable parts, ready for any concatenation syntax. |
| `securityRequirements(op, model)` | The operation's security as OR-alternatives of AND-sets, denormalized against the declared schemes. |
| `paginationItemSchema(pageSchema, itemsPointer, model)` | The raw element schema behind a pagination rule's `items` pointer — a `ref` element keeps its name. |
| `discriminatorCases(schema, model)` | `{ property, cases }` dispatch table for discriminated unions. |
| `isNullable(schema)` / `unwrapNullable(schema)` | Detect and strip `null` union members (`Optional[T]`, pointers, `Option<T>`). |
| `enumValues(schema)` | Values plus SCREAMING_SNAKE member-name suggestions. |
| `headerCoerceType(schema, model)` | Response-header coerce hint (`integer`/`number`/`boolean`/`string`) through refs, nullables, and allOf wrappers. |
| `casing` / `identifierFor(name, { style, reserved })` | camel/pascal/snake/screaming; keyword-safe identifiers (`RESERVED_WORDS.python/go/typescript` shipped). |
| `uniqueIdentifiers(names, { style, reserved, taken })` | The same, made unique among themselves and among names you already took — for a signature that takes one argument per parameter. |
| `Printer` | Indentation-aware text builder — no manual whitespace bookkeeping. |
| `docText(description)` | Description as trimmed lines for any comment syntax. |
| `schemaAtPointer(schema, pointer, model)` | Resolve an RFC 6901 JSON pointer over a schema (through refs and allOf) — e.g. a pagination `items` pointer to its element type. |
| `paginationRuleFor(op, config)` | The pagination rule that applies to an operation (per-op config > extension > fitting convention), normalized. |
| `renderReferencePage(model, options)` | The Markdown reference page a generator's `docs` hook returns — your `sample` hook supplies its call snippets. |
| `NotSupportedError` | Throw it to reject an option the generator can't honor — the CLI prints the message as a user error, not a crash. |
| `AUTHORING_HELPER_NAMES` | The list of the above (introspection). |
Worked example: the built-in `python` generator
(`packages/client-generator/src/generators/python/index.ts` in the Redocly CLI repo) is
authored with exactly this toolkit and nothing else — models via `flattenAllOf`/
`enumValues`/`discriminatorCases`, all code through `Printer`, every name through
`identifierFor(..., RESERVED_WORDS.python)`.
A generator that emits TypeScript may additionally use the source-text renderers from
`@redocly/client-generator/generate` — `tsType` (schema → type), `tsJsdoc`, `codeLiteral`,
`operationSignature`, `pascalCase`, `safeIdent`. There is no AST toolkit and no
`typescript` dependency: every generator prints text through `Printer`.
## The loop
1. Edit the generator.
2. Run `redocly generate-client`.
3. Inspect `git diff` of the generated output.
4. Repeat. Generated files are never hand-edited.
If you had to work around a **missing helper** or a wrong default, tell the user
and suggest filing an issue at https://github.com/Redocly/redocly-cli/issues —
include the generator snippet and the helper you expected to exist.
GitHubで見る