Using openapi-cli: custom preprocessing
Tags: api, openapi
Published: 2021-09-23 05:01:50

---
title: Using openapi-cli: custom preprocessing
published: true
date: 2021-08-17 03:44:37 UTC
tags: api, openapi
series: Using openapi-cli
canonical_url: https://www.aviskase.com/articles/2021/08/16/using-openapi-cli-custom-preprocessing/
---

The key feature of `openapi-cli` is its extensibility. There are three ways to extend it: [preprocessors, rules, and decorators](https://redoc.ly/docs/cli/custom-rules/). In comparison, [Spectral](https://meta.stoplight.io/docs/spectral) supports only custom rules.

In this tutorial, we’ll start with **preprocessors**. They are used to transform OpenAPI description document _before_ validation and linting. Mind you, documentation says they should be avoided, because custom preprocessors tend to be error prone.

## [<svg xmlns="http://www.w3.org/2000/svg" style="display: inline-block; vertical-align: middle;" width="1em" height="1em" viewbox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>](#example-extensible-enumerations)Example: extensible enumerations

In our Stargate Network API there is a [`Stargate.yaml` schema](https://github.com/aviskase/openapi-cli-examples/blob/bb95d479cc184221b38ff5d5371767f0b3f32f74/openapi/components/schemas/Stargate.yaml) with two properties defined with enumerations: `state` and `environment`. We might think that we captured all potential values, but we can’t be sure.

[Zalando API guidelines](https://opensource.zalando.com/restful-api-guidelines/#112) and [The Design of Web APIs by Arnaud Lauret](https://livebook.manning.com/book/the-design-of-web-apis/chapter-9/table9.1) warn us that enumerations can cause API compatibility breaks, especially with outputs.

Imagine the scenario. We generated SDK for the initial version of the API, and the generator was smart enough to use enumerations in the selected programming language. Some time later we push a new API version where `environment` can also be `STAR`. Customers, who are still on the first version of the SDK, won’t be able to process API responses containing this value, because most probably their clients will crash with something-something-uprocessable-entity exceptions.

Thus, similarly to Zalando, we will use a custom [vendor extension](https://spec.openapis.org/oas/v3.0.3.html#specification-extensions) to document possible values. I’m calling it `x-aviskase-enum` just in case not to introduce conflicts with other extensions. [Commit fa78766](https://github.com/aviskase/openapi-cli-examples/tree/fa78766d7ea2cd245740373efb951bffe7b2facf).

## [<svg xmlns="http://www.w3.org/2000/svg" style="display: inline-block; vertical-align: middle;" width="1em" height="1em" viewbox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>](#surfacing-values-in-the-documentation)Surfacing values in the documentation

Oops, now there is no way to see known values in the generated reference docs. The manual fix is to add these values to the `description`. But this is too cumbersome, especially if you’ll ever want to change presentation format.

Preprocessor for the rescue! What we want it to do:

1. Find all properties with `x-aviskase-enum` property.
2. Grab the list of values, format them nicely, and add it to the `description`. Make sure not to overwrite existing description if it’s already present!

### [<svg xmlns="http://www.w3.org/2000/svg" style="display: inline-block; vertical-align: middle;" width="1em" height="1em" viewbox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>](#adding-a-preprocessor)Adding a preprocessor

Let’s create a preprocessor first `./plugins/preprocessors/add-enum-to-description.js`:

```javascript
//@ts-check
module.exports = AddEnumToDescription;

/** @type { import('@redocly/openapi-core/src/visitors').Oas3Preprocessor } */
function AddEnumToDescription(options) {
  const vendorExtension = options.vendorExtension || 'x-enum';
  return {
    Schema(schema) {
      if (schema[vendorExtension]) {
        schema.description = appendToDescription(schema[vendorExtension], schema.description);
      }
    },
  };
}

function appendToDescription(values, description) {
  let additionalDescription = `Possible values: ${values.map(v => `\`${v}\``).join(', ')}`;
  if (description) {
    return `${description}\n\n${additionalDescription}`;
  }
  return additionalDescription;
}
```

What’s happening here:

- We export a preprocessor function `AddEnumToDescription` that accepts configuration `options`:
  - `vendorExtension` option is used to define extension name for extensible enumeration. If not provided, it equals to `x-enum`.
- All `openapi-cli` preprocessors should return a _visitor_. In our case, we specifically indicate to take into account only schema nodes.
- If a schema has defined extensible enumeration property, we update its description with formatted list of values.

<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewbox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>

---

**Note:** Per documentation, for type support you can use `import('@redocly/openapi-cli').Oas3Preprocessor}`. It never worked for me, so I use a “full path” `import('@redocly/openapi-core/src/visitors').Oas3Preprocessor`.

---

### [<svg xmlns="http://www.w3.org/2000/svg" style="display: inline-block; vertical-align: middle;" width="1em" height="1em" viewbox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>](#enabling-our-preprocessor-via-custom-plugin)Enabling our preprocessor via custom plugin

Let’s keep our preprocessor (and the future ones) bundled inside one plugin via `./plugins/custom-preprocessors.js`:

```javascript
//@ts-check
const AddEnumToDescription = require('./preprocessors/add-enum-to-description');
const id = 'custom-preprocessors';

/** @type { import('@redocly/openapi-core/src/config/config').PreprocessorsConfig } */
const preprocessors = {
  oas3: {
    'add-x-enum-to-description': AddEnumToDescription,
  },
};

module.exports = {
  id,
  preprocessors,
};
```

Here we defined plugin’s `id` and a mapping between preprocessor’s name (`add-x-enum-to-description`) and its implementation.

Then we need to add this custom plugin to the `.redocly.yaml` and to enable the preprocessor:

```yaml
...
lint:
  plugins:
    - './plugins/custom-preprocessors.js'
  preprocessors:
    custom-preprocessors/add-x-enum-to-description:
      vendorExtension: x-aviskase-enum
...
```

The final result is in [commit 6654926](https://github.com/aviskase/openapi-cli-examples/tree/665492641fa119292d2ef7aa2c6bd4a87266ff9b).

## [<svg xmlns="http://www.w3.org/2000/svg" style="display: inline-block; vertical-align: middle;" width="1em" height="1em" viewbox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>](#how-it-looks-in-the-docs)How it looks in the docs

Now, when we render a reference documentation, we can see all values with simple markdown formatting.

![Redoc with modified descriptions for extensible enumerations](https://www.aviskase.com/articles/2021/08/16/using-openapi-cli-custom-preprocessing/x_enum_render.png)

<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="none" viewbox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path></svg>

By the way, can you spot operations where we could have left normal `enum` and why?

## [<svg xmlns="http://www.w3.org/2000/svg" style="display: inline-block; vertical-align: middle;" width="1em" height="1em" viewbox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>](#why-use-a-preprocessor-here)Why use a preprocessor here?

As mentioned before, preprocessors might be brittle. We could have used decorator instead. I’ll show you the reason in the next article, but here is a sneak peek: we want to be able to [_lint_ modified descriptions](https://www.aviskase.com/articles/2021/09/06/using-openapi-cli-custom-rules/)!