ReferenceCLIapigenielint

lint

Detects stylistic and structural issues in OpenAPI, Swagger 2.0 and AsyncAPI documents, Protobuf (.proto) files and GraphQL schemas, improving their quality.

Usage

apigenie lint [OPTIONS] FILES

Description

The OpenAPI linter helps developers and teams create high-quality, consistent, and well-structured schemas. It achieves this by validating schemas against a set of configurable lint rules.

Lint Rules

Each rule in the linter focuses on a specific aspect of an OpenAPI schema. Here are some examples:

  • operation-description-defined: Ensures that all operations have meaningful descriptions.
  • operation-success-response: Ensures that all operations define a success response (e.g. 201 Created).
  • paths-max-count and paths-max-depth: Limit API complexity by checking the number of path items and depth (e.g. a number of segments) or URLs.

Each rule can have a severity level assigned, indicating its importance. Possible values for severity are error, warning and hint (also written info). All of them show up in the linter output, but only errors prevent the linter check from succeeding; hints are suggestions, which don’t count as warnings either. A rule may report a hint where its advice can’t be followed yet, such as schema-format-deprecated in OpenAPI 3.0 documents.

Some rules may accept options that adjust how they evaluate the schema.

The API Genie linter currently offers 53 built-in rules for OpenAPI, with over 20 additional rules under active development.

AsyncAPI documents

The linter also checks AsyncAPI 3.x documents; a file is treated as AsyncAPI when its root object has an asyncapi field. AsyncAPI documents have their own set of 31 built-in rules, ported from the OpenAPI ones where they make sense (for example contact-defined, license-defined, operation-tags-defined or unused-shared-component), plus AsyncAPI-specific ones such as channel-parameters-defined, channel-addresses-unique and server-hosts. They come in the same recommended, strict and all rulesets, and support custom rules, JavaScript functions, x-apigenie-lint-disable and --fix just like OpenAPI documents. OpenAPI and AsyncAPI files can be linted in a single run; see lint.asyncapi for their configuration.

Swagger 2.0 documents

The linter also checks Swagger 2.0 documents; a file is treated as Swagger when its root object has a swagger field. Swagger documents have their own set of 46 built-in rules, ported from the OpenAPI ones where they make sense (for example operation-id-unique, parameters-unique, path-parameters-defined or unused-shared-component), plus Swagger-specific ones: host-defined, schemes-secure, media-types-defined and swagger-validation, which reports the violations of the Swagger 2.0 specification that span several objects (undefined security schemes, unresolved references, several body parameters and so on). They come in the same recommended, strict and all rulesets, and support custom rules (whose formats is oas2), JavaScript functions, x-apigenie-lint-disable and --fix just like OpenAPI documents. See lint.swagger for their configuration.

GraphQL documents

The linter also checks GraphQL schemas and operations: files with the .graphql, .graphqls or .gql extension. GraphQL documents have their own set of 87 built-in rules: the type system and operation rules of the GraphQL specification, naming conventions, descriptions, deprecations, schema design (root types, mutation inputs and payloads, unreachable types, identifiers), the Relay connection conventions, ordering and spelling. Operations are also checked against a schema: unknown fields, types and arguments, required arguments, deprecated fields and selection depth. They come in the recommended, strict, all and relay rulesets. A # apigenie-lint-disable rule-a, rule-b comment disables rules for a definition and everything nested in it. --fix does not change GraphQL files. See lint.graphql for their configuration.

Rulesets

To simplify configuration and provide more flexibility, rules can be combined into rulesets. A ruleset is a named group of rules with assigned severity levels and options.

Disabling certain rules

There might be situations where you want to temporarily disable a rule for a specific part of a schema. This might be due to a legacy issue, ongoing development, or a specific API decision. To do this, annotate parts of your OpenAPI schema with the x-apigenie-lint-disable extension. The value of this extension must be a list of rule names you want to disable.

Rules can be disabled on any schema object, including OpenAPI object itself. Disabling a rule on specific schema object applies to that object and its descendants within the schema hierarchy.

For example, in the snippet below, the contact-defined rule is disabled on the Info schema object. However, it will also be enforced on the nested Contact object because Contact is a descendant of Info within the schema structure.

info:
  x-apigenie-lint-disable:
    - contact-defined

A couple of final remarks:

  • Disable rules as close to the problematic point in the schema as possible.
  • Disabling linter rules should be an exception, not the rule.

Custom rules

Besides the built-in rules, you can define your own rules in the lint section of the configuration file. A custom rule is data, in the format used by Spectral: it selects parts of the document with a JSONPath expression and checks each of them with a function.

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

A rule definition accepts these properties:

Property Description
given JSONPath expression, or a list of them, selecting the values to check. Required.
then What to check: an object or a list of objects with function, optional functionOptions and optional field. Required.
field Narrows each selected value: a member path (summary, a.b[0]), @key to check member names, or a JSONPath expression relative to the value ($.properties[*]).
message Message template; see below. Defaults to description, then to the function’s message.
description Human-readable description of the rule.
severity error, warn (default), info, hint or off. info and hint are reported as hints.
resolved Whether the rule sees the document with local $refs followed. Defaults to true.
formats Restricts the rule to document formats: oas2 (Swagger 2.0 documents), oas3, oas3_0, oas3_1, oas3_2, or, for AsyncAPI documents, aas3 and aas3_0, aas3_1, …

Messages may use {{error}} (the function’s message), {{description}}, {{path}} (JSON pointer of the problem), {{property}} (the last path segment) and {{value}}. Issues are reported where the offending value is defined, even when it is reached through a $ref. Custom rules honour x-apigenie-lint-disable like built-in ones. A custom rule can be switched off or have its severity changed later in the configuration (my-rule: off), but it cannot reuse the name of a built-in rule.

JSONPath

given and field expressions follow RFC 9535 with the extensions Spectral rulesets commonly use:

  • member unions without quotes: $.paths[*][get,put];
  • ~ at the end selects member names: $.paths[*]~;
  • filters written as JavaScript expressions: $..[?(@.type === 'object' && !@.properties)], with @property, @parent, @parentProperty, @path and $, and the string and array methods match, test, startsWith, endsWith, includes, indexOf, toLowerCase, toUpperCase, trim and length.

Script expressions such as [(@.length-1)] and Spectral aliases (#Name) are not supported.

Core functions

Function Options Checks that the value…
truthy, falsy none is (not) truthy in the JavaScript sense
defined, undefined none exists / does not exist
enumeration values is one of the listed values
length min, max has a length (string, array, object keys, number) within bounds
pattern match, notMatch (does not) match a JavaScript regular expression; /re/flags is accepted
casing type (flat, camel, pascal, kebab, cobol, snake, macro), disallowDigits, separator.char, separator.allowLeading follows a naming convention
alphabetical keyedBy lists keys or items in alphabetical order
or, xor properties has at least one / exactly one of the listed members
schema schema, dialect, allErrors is valid against a JSON Schema (draft 4 to 2020-12)
typedEnum none has enum values matching its type
unreferencedReusableObject reusableObjectsLocation has only members referenced somewhere in the document

JavaScript functions

When the core functions are not enough, write a function in JavaScript. List it under functions; API Genie loads <name>.js from the functions directory next to the configuration file (change it with functionsDir).

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

A function is an ES module whose default export receives the value, the options and a context, and returns a list of problems. The contract is the one of Spectral custom functions, so most of them work unchanged:

// functions/operationIdPrefix.js
import { createRulesetFunction } from '@stoplight/spectral-core';
import { casing } from '@stoplight/spectral-functions';

export default createRulesetFunction(
  {
    input: { type: 'string' },
    options: {
      type: 'object',
      properties: { prefix: { type: 'string' } },
      required: ['prefix'],
    },
  },
  (input, options, context) => {
    if (!input.startsWith(options.prefix)) {
      return [{ message: `must start with "${options.prefix}"` }];
    }
    return casing(input, { type: 'camel' }, context);
  },
);
  • createRulesetFunction validates functionOptions when the configuration is loaded and skips values that do not match input.
  • A result may carry a path (an array of keys and indexes) to point at a more specific location than context.path.
  • context provides path, rule (name, severity, resolved), document (formats, data) and documentInventory (resolved, unresolved).
  • Functions may be async.
  • A function that throws is reported as an issue of its rule.

Functions run in a sandbox: they can import other modules from the functions directory and the two built-in @stoplight modules, but have no access to the file system, network, processes or timers. Each call must finish within 5 seconds. Custom JavaScript functions are not available in the WebAssembly build.

Autofixing

Some issues found by the apigenie lint command can be automatically fixed. To do this, run the command with the --fix option. The fixed files keep their comments, unless --strip-comments is given.

Formatting results

Apart from rules, API Genie linter allows to customize the way it displays results. It ships with several built-in formatters, described below, in subsections.

Formatters differ in the way they present data, but all of them sort issues by:

  1. The line in the input OpenAPI schema file where they were found
  2. Severity level: warnings are displayed before errors.

Note: If you used ESLint you may notice that the list of formatters API Genie provides is close to that of ESLint. This is by no coincidence, as both tools aim to be usable in many different environments.

checkstyle

Outputs results in the Checkstyle format.

<?xml version="1.0" encoding="utf-8"?>
<checkstyle version="4.3">
    <file name="/apigenie/data/openapi.yaml">
        <error line="2" column="2" severity="error" message="Info summary is missing or empty" source="info-defined"/>
        <error line="12" column="4" severity="warning" message="Tag &apos;Articles&apos; has no meaningful description defined" source="tag-description-defined"/>
        <error line="13" column="4" severity="warning" message="Tag &apos;Comments&apos; has no meaningful description defined" source="tag-description-defined"/>
        <error line="14" column="4" severity="warning" message="Tag &apos;Favorites&apos; has no meaningful description defined" source="tag-description-defined"/>
        <error line="15" column="4" severity="warning" message="Tag &apos;Profile&apos; has no meaningful description defined" source="tag-description-defined"/>
    </file>
</checkstyle>

compact

Mimicks the default output of JSHint.

/apigenie/data/openapi.yaml: line 2, col 2, Error - Info summary is missing or empty. (info-defined)
/apigenie/data/openapi.yaml: line 12, col 4, Warning - Tag 'Articles' has no meaningful description defined. (tag-description-defined)
/apigenie/data/openapi.yaml: line 13, col 4, Warning - Tag 'Comments' has no meaningful description defined. (tag-description-defined)
/apigenie/data/openapi.yaml: line 14, col 4, Warning - Tag 'Favorites' has no meaningful description defined. (tag-description-defined)
/apigenie/data/openapi.yaml: line 15, col 4, Warning - Tag 'Profile' has no meaningful description defined. (tag-description-defined)

html

Prints results as a single HTML file. Issues are displayed in a table, every issue takes one table row.

<!DOCTYPE html>
<html>
<body>
  <section id="overview">
    <h1>API Genie Report</h1>
    <ul>
      <li>Total: 224</li>
      <li>Errors: 32</li>
      <li>Warnings: 192</li>
    </ul>
  </section>
  <section id="issues">
    <table>
      <caption>/apigenie/data/openapi.yaml</caption>
      <tr>
        <td class="pos">2:2</td>
        <td class="error">Error</td>
        <td>Info summary is missing or empty</td>
        <td>
          <a href="https://apigenie.pl/docs/linter/openapi/rules/info-defined">info-defined</a>
        </td>
      </tr>
      <tr>
        <td class="pos">12:4</td>
        <td class="warning">Warning</td>
        <td>Tag 'Articles' has no meaningful description defined</td>
        <td>
          <a href="https://apigenie.pl/docs/linter/openapi/rules/tag-description-defined">tag-description-defined</a>
        </td>
      </tr>
    </table>
</body>
</html>

json

Outputs lint results in JSON file. This format is useful if you want to process linter result programmatically. results has one item per file with issues; formatVersion is the version of the format, which changes only when a member is removed or changes meaning (see Compatibility). Its JSON Schema is apigenie-lint-report.json.

{
  "formatVersion": 1,
  "results": [
    {
      "filePath": "/apigenie/data/openapi.yaml",
      "messages": [
        {
          "column": 2,
          "line": 2,
          "message": "Info summary is missing or empty",
          "ruleId": "info-defined",
          "severity": "error"
        },
        {
          "column": 4,
          "line": 12,
          "message": "Tag 'Articles' has no meaningful description defined",
          "ruleId": "tag-description-defined",
          "severity": "warning"
        },
        ... more issues ...
      ]
    }
  ]
}

API Genie 0.9 and earlier wrote the bare results array.

junit

Outputs results to format compatible with the JUnit Jenkins plugin.

<?xml version="1.0" encoding="utf-8"?>
<testsuites>
  <testsuite package="apigenie" time="0" tests="224" errors="224" name="/apigenie/data/openapi.yaml">
    <testcase time="0" name="apigenie.info-defined" classname="apigenie.info-defined">
      <failure message="Info summary is missing or empty"><![CDATA[line 2, col 2, Error - Info summary is missing or empty. (info-defined)]]></failure>
    </testcase>
    <testcase time="0" name="apigenie.tag-description-defined" classname="apigenie.tag-description-defined">
      <failure message="Tag &apos;Articles&apos; has no meaningful description defined"><![CDATA[line 12, col 4, Warning - Tag 'Articles' has no meaningful description defined. (tag-description-defined)]]></failure>
    </testcase>
    <testcase time="0" name="apigenie.tag-description-defined" classname="apigenie.tag-description-defined">
      <failure message="Tag &apos;Comments&apos; has no meaningful description defined"><![CDATA[line 13, col 4, Warning - Tag 'Comments' has no meaningful description defined. (tag-description-defined)]]></failure>
    </testcase>
  </testsuite>
</testsuites>

sarif

Outputs results in SARIF 2.1.0, the format of GitHub code scanning and other static analysis dashboards. Files below the current directory are given relative to it (%SRCROOT%), so that code scanning finds them in the repository. A result whose JSON pointer is known has it as a logical location and in a fingerprint, which lets code scanning keep tracking the issue when lines move.

{
  "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
  "version": "2.1.0",
  "runs": [
    {
      "tool": {
        "driver": {
          "name": "apigenie",
          "informationUri": "https://apigenie.pl",
          "rules": [
            {
              "id": "info-defined",
              "helpUri": "https://apigenie.pl/docs/linter/openapi/rules/info-defined"
            },
            ... more rules ...
          ]
        }
      },
      "originalUriBaseIds": {
        "%SRCROOT%": { "uri": "file:///apigenie/" }
      },
      "results": [
        {
          "ruleId": "info-defined",
          "ruleIndex": 0,
          "level": "error",
          "message": { "text": "Info summary is missing or empty" },
          "locations": [
            {
              "physicalLocation": {
                "artifactLocation": { "uri": "data/openapi.yaml", "uriBaseId": "%SRCROOT%" },
                "region": { "startLine": 2, "startColumn": 2 }
              },
              "logicalLocations": [
                { "fullyQualifiedName": "#/info", "kind": "member" }
              ]
            }
          ],
          "partialFingerprints": {
            "apigenieLocation/v1": "info-defined|data/openapi.yaml|#/info"
          }
        },
        ... more issues ...
      ]
    }
  ]
}

stylish

This is the default formatter. It presents result as table, with values being highligted in different colors.

  764:6   warning  Unused shared component                                              unused-shared-component
  769:12  warning  Object schema does not define constraints restricting its size       schema-constraints
  769:12  error    Object schema does not disable additional properties                 schema-no-additional-properties
  777:6   warning  Unused shared component                                              unused-shared-component
  781:8   error    Schema with integer type does not define proper format               schema-format-defined
  781:8   warning  Integer schema does not define constraints restricting its range     schema-constraints
  785:6   warning  Unused shared component                                              unused-shared-component
  789:8   error    Schema with integer type does not define proper format               schema-format-defined
  789:8   warning  Integer schema does not define constraints restricting its range     schema-constraints
  795:6   warning  Unused shared component                                              unused-shared-component

  224 issues (32 errors, 192 warnings)
  0 errors, 6 warnings are potentially auto-fixable

tap

Outputs results to the Test Anything Protocol (TAP) specification format.

TAP version 14
1..1
not ok - /apigenie/data/openapi.yaml
  ---
  errorCount: 32
  issues:
    - column: 2
      line: 2
      message: Info summary is missing or empty
      ruleId: info-defined
      severity: error
    - column: 4
      line: 12
      message: Tag 'Articles' has no meaningful description defined
      ruleId: tag-description-defined
      severity: warning
    - column: 4
      line: 13
      message: Tag 'Comments' has no meaningful description defined
      ruleId: tag-description-defined
      severity: warning

unix

Outputs results to a format similar to many commands in UNIX-like systems. If you use grep or awk, this format may be best for you.

/apigenie/data/openapi.yaml:2:2: Info summary is missing or empty. [Error/info-defined]
/apigenie/data/openapi.yaml:12:4: Tag 'Articles' has no meaningful description defined. [Warning/tag-description-defined]
/apigenie/data/openapi.yaml:13:4: Tag 'Comments' has no meaningful description defined. [Warning/tag-description-defined]
/apigenie/data/openapi.yaml:14:4: Tag 'Favorites' has no meaningful description defined. [Warning/tag-description-defined]
/apigenie/data/openapi.yaml:15:4: Tag 'Profile' has no meaningful description defined. [Warning/tag-description-defined]
/apigenie/data/openapi.yaml:16:4: Tag 'Tags' has no meaningful description defined. [Warning/tag-description-defined]

visualstudio

Outputs results to format compatible with the integrated terminal of the Visual Studio IDE.

/apigenie/data/openapi.yaml(2,2): error info-defined : Info summary is missing or empty.
/apigenie/data/openapi.yaml(12,4): warning tag-description-defined : Tag 'Articles' has no meaningful description defined.
/apigenie/data/openapi.yaml(13,4): warning tag-description-defined : Tag 'Comments' has no meaningful description defined.
/apigenie/data/openapi.yaml(14,4): warning tag-description-defined : Tag 'Favorites' has no meaningful description defined.
/apigenie/data/openapi.yaml(15,4): warning tag-description-defined : Tag 'Profile' has no meaningful description defined.
/apigenie/data/openapi.yaml(16,4): warning tag-description-defined : Tag 'Tags' has no meaningful description defined.

Options

–config FILE

Optional path to the configuration file. If not specified, the default configuration is used. For details, see the configuration section.

–fix

If possible, automatically fixes problems it finds. Modified file is written under the same name as the original. The exit status reflects the issues that remain after the fixes. The YAML comments of the fixed OpenAPI, Swagger and AsyncAPI documents are kept with the values they belong to; those of values a fix removes go with them. Protobuf files are fixed by editing their text, which keeps everything outside the edited declarations.

–strip-comments

With --fix, drops the YAML comments of the OpenAPI, Swagger and AsyncAPI documents it rewrites, instead of keeping them. Files without fixes are not rewritten.

–formatter NAME

Sets the formatter to display the linter result. Supported formatter names are:

  • checkstyle
  • compact
  • html
  • json
  • junit
  • sarif
  • stylish - the default formatter
  • tap
  • unix
  • visualstudio

–graphql-schema FILE

A GraphQL schema file that the operations and fragments of the linted GraphQL files are checked against. Can be repeated for a schema split across files. When given, it replaces the files of the schema option of the configuration; relative paths resolve against the current directory. With --watch, the schema files are watched too.

apigenie lint --graphql-schema schema.graphql queries/*.graphql

Exit status

The command exits with 0 when no issue of severity error is reported, even if warnings or hints are, and with 1 when at least one is. With --fix, only the issues that remain after the fixes count. It exits with 2 when the configuration or an input file can’t be read or parsed, or an input is not a document of its specification. With --watch the command keeps running and never exits on issues. See the exit statuses shared by the three binaries.

Configuration

The linter stores its configuration under the #/commands/linter key in the configuration file. See the global configuration section to understand how this file is structured.

The linter configuration allows to apply rulesets, enable or disable certain rules, modify rule severity levels, or modify rules’ options.

Example: a minimal confiration illustrating all possible variants of configuring rules.

version: '1.0'
commands:
  lint:
    extends:
      - recommended
      # ... more rulesets to extend from ...
    rules:
      info-defined: off
      paths-max-count:
        - error
        - 30
      schema-datetime-suffix:
        - error
        - At
      tags-unique: error
      # ... more rules ...

If the configuration file is not specified, linter uses the default configuration:

version: '1.0'
commands:
  lint:
    extends:
      - recommended

lint.extends

This block lists rulesets whose configuration will be used and combined to create the final set of rules, rule severity levels and options.

The value of this block, if present, must be an array of supported ruleset names. Linter will read all rulesets’ configurations, in the order they are listed. If two rulesets contain the same rule, linter uses the configuration from the ruleset that was specified later in the list.

lint.rules

These are overrides to the rules’ configurations from rulesets.

If present, value of this block must be a map. Keys of the map must be valid rule names, and values can be either strings or two-element arrays.

For valid rule names see the rule index page.

lint.rules.RULE_ID

Such blocks configure a rule named RULE_ID. Each block may be a string or a two-element array.

  • String: If it a string, and must be one of:
    • off - to disable rule
    • warning - to set rule severity to warning
    • error - to set rule severity to error
  • Array: If it is an array, it must contain two elements:
    • The first element must be a valid rule severity (either warning or error).
    • The second element is rule-specific value, containing rule’s options.

See the documentation of each rule to find what options it accepts.

Examples

  • Disables the info-defined rule:
version: '1.0'
commands:
  lint:
    rules:
      info-defined: off
  • configure the paths-max-count rule to issue an error when the number of path items is larger than 30. If the number is below or equal to 30, the rule does not produce an error or warning.
version: '1.0'
commands:
  lint:
    rules:
      paths-max-count:
        - error
        - 30

lint.asyncapi

The configuration of the linter for AsyncAPI documents. It accepts the same keys as the lint block itself (extends, rules, functions and functionsDir), with rule names of the AsyncAPI rules. The OpenAPI settings of the lint block do not apply to AsyncAPI documents; without this block, AsyncAPI documents are linted with the recommended ruleset.

version: '1.0'
commands:
  lint:
    extends: [recommended]
    asyncapi:
      extends: [strict]
      rules:
        parameter-naming: [warning, camel]
        server-hosts: off

lint.swagger

The configuration of the linter for Swagger 2.0 documents. It accepts the same keys as the lint block itself (extends, rules, functions and functionsDir), with rule names of the Swagger rules. The OpenAPI settings of the lint block do not apply to Swagger documents; without this block, Swagger documents are linted with the recommended ruleset.

version: '1.0'
commands:
  lint:
    extends: [recommended]
    swagger:
      extends: [strict]
      rules:
        schemes-secure: [error, { upgradeInsecure: true }]
        swagger-validation: [error, { checks: [unresolved-reference, security-scheme-defined] }]
        paths-sorted: off

lint.graphql

The configuration of the linter for GraphQL documents. It accepts extends, rules, self_contained (whether each document holds every type and fragment it references, so that references to missing ones are reported) and schema (a path, or a list of paths, of the schema files operations are checked against; relative paths resolve against the directory of the configuration file; –graphql-schema replaces them). Without schema, operations are checked against the type system of their own document. Without this block, GraphQL documents are linted with the recommended ruleset.

version: '1.0'
commands:
  lint:
    graphql:
      extends: [recommended, relay]
      schema: schema.graphql
      rules:
        field-description-defined: warning
        selection-set-depth: [error, { maxDepth: 5 }]
        no-deprecated: off

See Also