# Migrate from Spectral

Source: https://apigenie.pl/docs/guides/migrate-from-spectral

This guide moves a repository that lints its API descriptions with Spectral to `apigenie lint`:
the built-in rules, your own rules and functions, the report in CI, and what has no counterpart.
It starts from Spectral 6.17 with its `spectral:oas` and `spectral:asyncapi` rulesets and an
OpenAPI file, `openapi.yaml`.

You need the CLI installed; the [Quickstart](/docs/quickstart) shows how. Keep Spectral installed
until the last step, so you can compare the two reports on the same files.

## What changes and what stays

| | Spectral | API Genie |
| --- | --- | --- |
| Command | `spectral lint openapi.yaml` | `apigenie lint openapi.yaml` |
| Configuration | `.spectral.yaml`, `.spectral.json` or `.spectral.js` | `apigenie.yaml`, section `commands.lint` ([Configuration](/docs/reference/cli/configuration)) |
| Built-in rules | `spectral:oas` (57 rules), `spectral:asyncapi` (55) | rulesets per standard: `recommended`, `strict`, `all`; 64 rules for [OpenAPI](/docs/linter/openapi), 48 for [Swagger 2.0](/docs/linter/swagger), 34 for [AsyncAPI](/docs/linter/asyncapi) |
| Severities | `error`, `warn`, `info`, `hint`, `off` | `error`, `warning`, `hint` (or `info`), `off`; a custom rule writes `warn` |
| Custom rules | `given`, `then`, functions | the same format, under `commands.lint.rules` |
| Custom functions | JavaScript, `functionsDir` | JavaScript in `functions/`, listed under `functions` |
| Disabling a rule in one place | `overrides` by file in the ruleset | `x-apigenie-lint-disable` in the document |
| Report formats | `-f json`, `junit`, `sarif`, `html`, ... | `--formatter json`, `junit`, `sarif`, `html`, ... |
| Exit status | 1 at `--fail-severity` and above, `error` by default | 1 on an error, 0 on warnings only, 2 on a file it cannot read |
| Fixes | none | `--fix` for the issues that have one |

## Run both on the same file

```sh
spectral lint openapi.yaml
apigenie lint openapi.yaml
```

```text
  764:6   warning  Unused shared component                                 unused-shared-component
  781:8   error    Schema with integer type does not define proper format  schema-format-defined

  2 issues (1 error, 1 warning)
  0 errors, 1 warning are potentially auto-fixable
```

Each line gives the position, the severity, the message and the rule; every rule has a page in the
[linter rules](/docs/linter) that says what it checks and why. The two reports differ, and not
only in the names: without configuration API Genie applies the
[recommended ruleset](/docs/linter/openapi/rulesets/recommended), 43 rules of which some have no
Spectral counterpart, and leaves out some of what `spectral:oas` reports. The next two sections
say which.

## Replace the ruleset

A `.spectral.yaml` that extends the built-in ruleset and adjusts a few rules:

```yaml
extends: spectral:oas
rules:
  operation-description: error
  info-contact: off
  oas3-server-not-example.com: warn
```

becomes, in `apigenie.yaml` at the root of the repository:

```yaml
version: '1.0'
commands:
  lint:
    extends: [recommended]
    rules:
      operation-description-defined: error
      contact-defined: off
```

- `extends` names API Genie's rulesets, not Spectral's. `recommended` is the default; `strict`
  turns on more rules with higher severities, `all` every rule. The rulesets of each standard are
  listed in the [linter rules](/docs/linter).
- A rule takes its name from the table below. Spectral's `warn` is `warning` for a built-in rule;
  `hint` and `off` stay.
- A rule with no counterpart, such as `oas3-server-not-example.com`, has no line.
- An OpenAPI 3.x file, a Swagger 2.0 file and an AsyncAPI file are linted by three linters with
  rules of their own, so one configuration covers a repository that mixes them; Spectral picks the
  ruleset by the document's format.

## The rules of spectral:oas, by name

The rules Spectral turns on by default, and their counterparts in the OpenAPI linter. A rule
marked *not checked* has no counterpart today: drop its line from the configuration.

| Spectral | API Genie | Note |
| --- | --- | --- |
| `operation-success-response` | [operation-success-response](/docs/linter/openapi/rules/operation-success-response) | |
| `operation-operationId` | [operation-id-defined](/docs/linter/openapi/rules/operation-id-defined) | |
| `operation-operationId-unique` | [operation-id-unique](/docs/linter/openapi/rules/operation-id-unique) | |
| `operation-operationId-valid-in-url` | not checked | |
| `operation-parameters` | [parameters-unique](/docs/linter/openapi/rules/parameters-unique) | |
| `operation-tags` | [operation-tags-defined](/docs/linter/openapi/rules/operation-tags-defined) | |
| `operation-tag-defined` | not checked | a tag an operation uses without the document declaring it |
| `operation-description` | [operation-description-defined](/docs/linter/openapi/rules/operation-description-defined) | also rejects descriptions under 10 characters |
| `path-params` | [path-parameters-defined](/docs/linter/openapi/rules/path-parameters-defined) | [path-parameters-on-items](/docs/linter/openapi/rules/path-parameters-on-items) asks for them on the path item |
| `path-declarations-must-exist`, `path-keys-no-trailing-slash` | [paths-well-formed](/docs/linter/openapi/rules/paths-well-formed) | |
| `path-not-include-query` | not checked | |
| `duplicated-entry-in-enum` | [schema-enum-no-duplicates](/docs/linter/openapi/rules/schema-enum-no-duplicates) | |
| `typed-enum` | [schema-enum-types](/docs/linter/openapi/rules/schema-enum-types) | |
| `array-items`, `no-$ref-siblings` | not checked | |
| `info-contact` | [contact-defined](/docs/linter/openapi/rules/contact-defined) | asks for `name`, and `url` or `email`, as Spectral's `contact-properties` does |
| `info-description` | [info-defined](/docs/linter/openapi/rules/info-defined) | asks for `summary`, `description` and `version` |
| `no-eval-in-markdown`, `no-script-tags-in-markdown` | [safe-markdown](/docs/linter/openapi/rules/safe-markdown) | |
| `openapi-tags-uniqueness` | [tags-unique](/docs/linter/openapi/rules/tags-unique) | |
| `oas3-api-servers` | [servers-defined](/docs/linter/openapi/rules/servers-defined) | |
| `oas3-server-trailing-slash`, `oas3-server-variables` | not checked | [server-urls](/docs/linter/openapi/rules/server-urls) checks for `http` and upper-case letters instead |
| `oas3-operation-security-defined` | [operation-security-defined](/docs/linter/openapi/rules/operation-security-defined) | a different check: every operation has a security requirement; Spectral checks that the named schemes exist |
| `oas3-schema` | [openapi-json-schema-validation](/docs/linter/openapi/rules/openapi-json-schema-validation) | |
| `oas3-unused-component` | [unused-shared-component](/docs/linter/openapi/rules/unused-shared-component) | follows references transitively, so a component used only by an unused one is reported too |
| `oas3-examples-value-or-externalValue`, `oas3-valid-media-example`, `oas3-valid-schema-example` | not checked | examples are not validated against their schema |
| `oas3-callbacks-in-callbacks`, `oas3_1-servers-in-webhook`, `oas3_1-callbacks-in-webhook`, `oas3_2-no-deprecated-xml-attribute` | not checked | |

The rules Spectral leaves off by default:

| Spectral | API Genie |
| --- | --- |
| `contact-properties` | [contact-defined](/docs/linter/openapi/rules/contact-defined) |
| `info-license` | [license-defined](/docs/linter/openapi/rules/license-defined) |
| `license-url` | not checked |
| `openapi-tags` | not checked |
| `openapi-tags-alphabetical` | [tags-sorted](/docs/linter/openapi/rules/tags-sorted) |
| `operation-singular-tag` | not checked |
| `tag-description` | [tag-description-defined](/docs/linter/openapi/rules/tag-description-defined) |
| `oas3-parameter-description` | [parameter-description-defined](/docs/linter/openapi/rules/parameter-description-defined) |
| `oas3-server-not-example.com` | not checked |

The `oas2-*` rules apply to Swagger 2.0 files, which API Genie lints with the
[Swagger 2.0 linter](/docs/linter/swagger): `oas2-schema` is
[swagger-validation](/docs/linter/swagger/rules/swagger-validation), `oas2-api-host` is
[host-defined](/docs/linter/swagger/rules/host-defined), `oas2-unused-definition` is
[unused-shared-component](/docs/linter/swagger/rules/unused-shared-component), and the rules that
share a name with an OpenAPI rule check the same thing. The AsyncAPI rules of `spectral:asyncapi`
map onto the [AsyncAPI linter](/docs/linter/asyncapi) the same way: `asyncapi-info-contact` is
[contact-defined](/docs/linter/asyncapi/rules/contact-defined), `asyncapi-channel-parameters` is
[channel-parameters-defined](/docs/linter/asyncapi/rules/channel-parameters-defined),
`asyncapi-unused-components-schema` is
[unused-shared-component](/docs/linter/asyncapi/rules/unused-shared-component).

## What the recommended ruleset adds

Rules of `recommended` that `spectral:oas` has nothing for; each has a page that says what it
checks and why:

- [schema-format-defined](/docs/linter/openapi/rules/schema-format-defined),
  [schema-format-type-mismatch](/docs/linter/openapi/rules/schema-format-type-mismatch) and
  [schema-format-deprecated](/docs/linter/openapi/rules/schema-format-deprecated): numbers and
  strings say their format, and the format fits the type.
- [schema-constraints](/docs/linter/openapi/rules/schema-constraints) and
  [schema-no-additional-properties](/docs/linter/openapi/rules/schema-no-additional-properties):
  strings, numbers, arrays and objects have bounds.
- [schema-no-contradictions](/docs/linter/openapi/rules/schema-no-contradictions),
  [schema-oneof-no-overlap](/docs/linter/openapi/rules/schema-oneof-no-overlap) and
  [schema-conditional-effective](/docs/linter/openapi/rules/schema-conditional-effective): schemas
  that reject every value, `oneOf` branches a value matches twice, `if` without effect.
- [standard-response-codes](/docs/linter/openapi/rules/standard-response-codes),
  [unambiguous-response-codes](/docs/linter/openapi/rules/unambiguous-response-codes) and
  [no-content-response-defined](/docs/linter/openapi/rules/no-content-response-defined): the
  responses an operation declares.
- [paths-max-depth](/docs/linter/openapi/rules/paths-max-depth),
  [paths-max-count](/docs/linter/openapi/rules/paths-max-count),
  [parameter-naming](/docs/linter/openapi/rules/parameter-naming) and
  [schema-datetime-suffix](/docs/linter/openapi/rules/schema-datetime-suffix): the shape of the
  URLs and the names.

Outside `recommended`: [spell-check](/docs/linter/openapi/rules/spell-check) for the texts written
for humans, [schema-keyword-support](/docs/linter/openapi/rules/schema-keyword-support) for the
JSON Schema keywords the tools around the document ignore, and the
[privacy ruleset](/docs/linter/openapi/rulesets/privacy) for credentials and personal data in
URLs. `extends: [recommended, privacy]` adds the last one.

## Carry your custom rules over

A custom rule keeps its definition. This rule of `.spectral.yaml`:

```yaml
rules:
  operation-summary:
    description: Operations must have a summary
    message: "{{property}} is missing at {{path}}"
    severity: error
    given: "$.paths[*][get,put,post,delete,patch]"
    then:
      field: summary
      function: truthy
```

goes under `commands.lint.rules` of `apigenie.yaml` as it is:

```yaml
version: '1.0'
commands:
  lint:
    extends: [recommended]
    rules:
      operation-summary:
        description: Operations must have a summary
        message: "{{property}} is missing at {{path}}"
        severity: error
        given: "$.paths[*][get,put,post,delete,patch]"
        then:
          field: summary
          function: truthy
```

What carries over, as [custom rules](/docs/reference/cli/lint#custom-rules) documents it:

- `given`, `then` with `field`, `function` and `functionOptions`, `message` with `{{error}}`,
  `{{description}}`, `{{path}}`, `{{property}}` and `{{value}}`, `description`, `severity`,
  `resolved` and `formats` (`oas2`, `oas3`, `oas3_0`, `oas3_1`, `oas3_2`, `aas3`, ...).
- The core functions: `truthy`, `falsy`, `defined`, `undefined`, `enumeration`, `length`,
  `pattern`, `casing`, `alphabetical`, `or`, `xor`, `schema`, `typedEnum` and
  `unreferencedReusableObject`, with their options.
- JSONPath as Spectral rulesets write it: member unions without quotes, `~` for member names, and
  filters written as JavaScript expressions with `@property`, `@parent`, `@path` and the string
  methods ([JSONPath](/docs/reference/cli/lint#jsonpath)).

What does not:

- `aliases` and the `#Name` references to them: write the path out.
- Script expressions such as `[(@.length-1)]`.
- `extends` with a file or a URL: the rules of a shared ruleset go into the configuration, or into
  one file that every repository passes with `--config`.
- `overrides`, `parserOptions`, `documentationUrl` and `except`.
- A custom rule named like a built-in rule: rename it.

## Carry your functions over

A JavaScript function written for Spectral works unchanged in most cases: it is an ES module whose
default export receives the value, the options and a context, and
`@stoplight/spectral-core` and `@stoplight/spectral-functions` are built in. Put the files in
`functions/` next to `apigenie.yaml` (`functionsDir` changes the directory) and list them:

```yaml
commands:
  lint:
    functions: [operationIdPrefix]
    rules:
      operation-id-prefix:
        given: "$.paths[*][*].operationId"
        then:
          function: operationIdPrefix
          functionOptions: { prefix: api }
```

Functions run in a sandbox with no file system, network, processes or timers, and a call must
finish within 5 seconds. A function that reads a file or fetches a URL has to go; one that needs
another module imports it from the functions directory.
[JavaScript functions](/docs/reference/cli/lint#javascript-functions) has the contract.

## Disable a rule in one place

Spectral turns a rule off for some files with `overrides` in the ruleset. API Genie turns it off
where the issue is, in the document, with `x-apigenie-lint-disable` on the object and its
descendants:

```yaml
paths:
  /legacy/export:
    x-apigenie-lint-disable:
      - operation-id-defined
    get:
      summary: Export, as it was
```

Disable a rule as close to the issue as possible; [disabling rules](/docs/reference/cli/lint#disabling-rules-in-a-document)
says more.

## The report in CI

The formats that keep their name: `-f json`, `junit`, `sarif`, `html` and `stylish` become
`--formatter json`, `junit`, `sarif`, `html` and `stylish`. API Genie adds `checkstyle`, `compact`,
`tap`, `unix` and `visualstudio`; Spectral's `text`, `pretty`, `teamcity`, `github-actions`,
`code-climate`, `gitlab` and `markdown` have no counterpart. For GitLab, `junit` is what the
merge request reads; for GitHub code scanning, `sarif`
([formatting results](/docs/reference/cli/lint#formatters)).

`--fail-severity` has no counterpart: the command exits with 1 when an error is reported and with
0 on warnings alone ([exit status](/docs/reference/cli/lint#exit-status)). To fail the build on a
rule, give it `error` in the configuration. `-r ruleset.yaml` is `--config apigenie.yaml`.

On GitHub Actions, `stoplightio/spectral-action` becomes the
[GitHub Action](/docs/ci/github-action), which installs the CLI and shows the issues on the lines
of the pull request:

```yaml
on: pull_request
jobs:
  api:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: mykulyak/api-genie-action@v0
        with:
          command: lint
          files: openapi.yaml
```

## Remove Spectral

When the two reports agree on what matters to you: delete `.spectral.yaml`, remove
`@stoplight/spectral-cli` from the development dependencies, and replace the Spectral step of the
pipeline with the one above. The fixes the linter offers are a `--fix` away:

```sh
apigenie lint --fix openapi.yaml
```
