# Lint Rules Reference

> **Scope of this page**
>
> This page documents the Zuplo **gateway** runtime (API, AI, and MCP gateways), not the Developer Portal.
>
> It applies to every Zuplo hosting option: Managed Edge, Managed Dedicated, and Self-Hosted. See [Hosting options](https://zuplo.com/docs/articles/hosting-options).

Each rule has an ID, a default severity, and the presets that include it. Use
the ID to change the rule's severity, limit it to some routes, or turn it off in
[`zuplo.lint.jsonc`](./api-lint-configuration.mdx#rules). To add rules of your
own, see [Custom lint rules](./api-lint-custom-rules.mdx).

## Rules by preset

| Rule                                                                 | Severity   | recommended | strict | security | Auto-fix |
| -------------------------------------------------------------------- | ---------- | :---------: | :----: | :------: | :------: |
| [`operation-operationId`](#operation-operationid)                    | error      |      ✓      |   ✓    |    ✓     |    ✓     |
| [`operation-description`](#operation-description)                    | warn       |      ✓      |   ✓    |    ✓     |          |
| [`operation-tags`](#operation-tags)                                  | warn       |      ✓      |   ✓    |    ✓     |          |
| [`path-params`](#path-params)                                        | error      |      ✓      |   ✓    |    ✓     |          |
| [`no-ambiguous-paths`](#no-ambiguous-paths)                          | error      |      ✓      |   ✓    |    ✓     |          |
| [`zuplo/no-undefined-policies`](#zuplono-undefined-policies)         | error      |      ✓      |   ✓    |    ✓     |          |
| [`zuplo/no-empty-inbound-policies`](#zuplono-empty-inbound-policies) | warn       |      ✓      |   ✓    |    ✓     |    ✓     |
| [`zuplo/require-route-label`](#zuplorequire-route-label)             | warn       |      ✓      |   ✓    |    ✓     |          |
| [`zuplo/schema-policies`](#zuploschema-policies)                     | error      |      ✓      |   ✓    |    ✓     |          |
| [`zuplo/schema-policy-options`](#zuploschema-policy-options)         | warn       |      ✓      |   ✓    |    ✓     |          |
| [`zuplo/schema-x-zuplo-route`](#zuploschema-x-zuplo-route)           | error      |      ✓      |   ✓    |    ✓     |          |
| [`zuplo/require-policy`](#zuplorequire-policy)                       | per policy |             |   ✓    |    ✓     |          |
| [`zuplo/require-cors-policy`](#zuplorequire-cors-policy)             | error      |             |   ✓    |          |          |
| [`info-description`](#info-description)                              | warn       |             |   ✓    |          |          |
| [`info-contact`](#info-contact)                                      | warn       |             |   ✓    |          |          |
| [`no-http-verbs-in-path`](#no-http-verbs-in-path)                    | error      |             |   ✓    |          |          |

Two more IDs can appear in results without being rules you configure:

- `breaking-change` — reported by
  [breaking change detection](./api-lint-breaking-changes.mdx).
- `lint-execution-error` — a rule failed to run against a file. This usually
  means the file isn't valid JSON or isn't valid OpenAPI, so it has no line
  number (`0:0`). Fix the file and run the linter again.

## General OpenAPI rules

The Zuplo presets also include these general OpenAPI rules. Each one catches a
real defect and stays quiet on a typical Zuplo project.

| Rule                                 | Severity | Presets | Reports                                                           |
| ------------------------------------ | -------- | ------- | ----------------------------------------------------------------- |
| `duplicate-paths`                    | error    | all     | The same path defined twice; only the last definition is kept.    |
| `operation-operationId-unique`       | error    | all     | Two operations with the same `operationId`.                       |
| `operation-parameters`               | error    | all     | The same parameter declared twice on an operation.                |
| `path-declarations-must-exist`       | error    | all     | An empty path parameter, such as `/users/{}`.                     |
| `path-not-include-query`             | error    | all     | A query string in a path key.                                     |
| `oas3-operation-security-defined`    | error    | all     | A `security` entry with no matching `securitySchemes` definition. |
| `oas-schema-check`                   | error    | all     | A schema with an invalid `type`, such as `"string "`.             |
| `no-eval-in-markdown`                | error    | all     | `eval()` in a Markdown description.                               |
| `no-script-tags-in-markdown`         | error    | all     | `<script>` in a Markdown description.                             |
| `operation-operationId-valid-in-url` | warn     | all     | An `operationId` with characters that aren't URL-friendly.        |
| `operation-success-response`         | warn     | all     | An operation without a `2xx` or `3xx` response.                   |
| `oas3-valid-schema-example`          | warn     | all     | An example that doesn't match its schema.                         |
| `typed-enum`                         | warn     | all     | An enum value that doesn't match the enum's type.                 |
| `duplicated-entry-in-enum`           | warn     | all     | A repeated enum value.                                            |
| `nullable-enum-contains-null`        | warn     | all     | A nullable enum without `null` in its values.                     |
| `path-keys-no-trailing-slash`        | warn     | all     | A path ending in `/`.                                             |
| `no-request-body`                    | warn     | all     | A request body on a `GET` or `DELETE` operation.                  |
| `operation-4xx-response`             | warn     | strict  | An operation without a `4xx` error response.                      |
| `oas3-parameter-description`         | warn     | strict  | A parameter without a description.                                |
| `component-description`              | warn     | strict  | A component without a description.                                |
| `operation-tag-defined`              | warn     | strict  | An operation tag missing from the top-level `tags` list.          |
| `oas3-unused-component`              | warn     | strict  | A component nothing references.                                   |
| `required-fields-defined`            | warn     | strict  | A `required` property that isn't declared in `properties`.        |
| `no-unnecessary-combinator`          | warn     | strict  | An `allOf`, `anyOf`, or `oneOf` with a single item.               |

These rules come from vacuum, the engine behind `zuplo lint`. To use vacuum's
other general OpenAPI rules, or its OWASP security rules, extend `vacuum:oas` or
`spectral:owasp`; see [`extends`](./api-lint-configuration.mdx#extends). Run
`npx zuplo lint --list-rules` to see every rule and which rulesets include it.

## OpenAPI rules

These rules check the OpenAPI document itself. They apply to every operation,
whether or not it has an `x-zuplo-route` block.

### `operation-operationId`

Every operation must have an `operationId`. Zuplo uses it to identify the route,
and code generators and the Developer Portal use it to name operations.

```txt
6:14  error  Every operation must have an operationId: `operationId` must be set  operation-operationId
```

**Fix:** Add a unique `operationId` to the operation, or run `zuplo lint --fix`
to generate one from the method and path:

```json
"/todos/{id}": {
  "delete": {
    "operationId": "deleteTodosById",
```

### `operation-description`

Operations should have a `description`. The Developer Portal shows it as the
operation's documentation.

```txt
6:14  warn  Operations should have descriptions: `description` must be set  operation-description
```

**Fix:** Add a `description` that explains what the operation does.

### `operation-tags`

Operations should have at least one tag. The Developer Portal groups operations
by tag.

```txt
6:14  warn  Operations should have tags: `tags` must be set  operation-tags
```

**Fix:** Add a `tags` array, for example `"tags": ["Todos"]`.

### `path-params`

Every parameter in a path template must be declared, either on the path or on
the operation. The rule also reports two paths that differ only in the names of
their parameters.

```txt
19:5  error  parameter named `id` must be defined as part of the path `/todos/{id}` definition, or in the `DELETE` operation(s)  path-params
```

**Fix:** Declare the parameter with `"in": "path"` and `"required": true`:

```json
"/todos/{id}": {
  "delete": {
    "parameters": [
      {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": { "type": "string" }
      }
    ],
```

### `no-ambiguous-paths`

Two paths can't match the same request. `/users/{id}` and `/users/{name}` are
ambiguous: a request to `/users/42` matches both.

```txt
6:5  error  paths are ambiguous with one another: `/users/{id}` (GET) and `/users/{name}` (GET)  no-ambiguous-paths
```

This pair is also reported by [`path-params`](#path-params), so turning off one
of the two rules doesn't silence it.

**Fix:** Merge the two paths into one, or change one so it no longer overlaps,
for example `/users/by-name/{name}`.

### `info-description`

_`zuplo:strict` only._ The document's `info` object must have a `description`.

```txt
3:11  warn  API info must have a description: `description` must be set  info-description
```

**Fix:** Add `info.description`.

### `info-contact`

_`zuplo:strict` only._ The document's `info` object should have a `contact`.

```txt
3:11  warn  API info should have contact info: `contact` must be set  info-contact
```

**Fix:** Add `info.contact`, for example
`"contact": { "name": "API Support", "email": "api@example.com" }`.

### `no-http-verbs-in-path`

_`zuplo:strict` only._ Path segments shouldn't be HTTP methods. The HTTP method
already says what the operation does.

```txt
7:19  error  path `/users/get` contains an HTTP Verb `get`  no-http-verbs-in-path
```

The rule matches whole segments, so `/users/get` and `/post` are reported, while
`/getUsers` and `/delete-user` aren't.

**Fix:** Move the action into the method: `GET /users` instead of
`GET /users/get`.

## Zuplo route rules

These rules check the `x-zuplo-route` block of each operation. Operations
without an `x-zuplo-route` block are skipped.

### `zuplo/no-undefined-policies`

Every policy name in a route's `policies.inbound` or `policies.outbound` list
must be defined in `config/policies.json`. A missing policy fails the build.

```txt
15:56  error  Policy "rate-limit-inbound" is referenced in route but not defined in policies.json  zuplo/no-undefined-policies
```

**Fix:** Correct the spelling of the policy name in the route, or add the policy
to `config/policies.json`. The rule only runs when `config/policies.json`
defines at least one policy.

### `zuplo/no-empty-inbound-policies`

A route shouldn't have an empty `policies.inbound` array. An empty array has the
same effect as no array, and it's usually left behind after removing a policy.

```txt
31:36  warn  Route delete /todos/{id} has an empty inbound policies array  zuplo/no-empty-inbound-policies
```

**Fix:** Remove the empty array, or run `zuplo lint --fix`.

### `zuplo/require-route-label`

Routes should have a `label` in `x-zuplo-route`. The label names the route in
the Zuplo Portal's route designer, logs, and analytics.

```txt
6:14  warn  Route get /todos should have a label in x-zuplo-route  zuplo/require-route-label
```

**Fix:** Add `"label": "List todos"` to the `x-zuplo-route` block.

### `zuplo/require-policy`

_`zuplo:strict` and `zuplo:security`, or any preset with
[`requiredPolicies`](./api-lint-configuration.mdx#requiredpolicies)._ Every
route must include a policy of each required type.

| Preset           | Authentication | Rate limiting |
| ---------------- | -------------- | ------------- |
| `zuplo:strict`   | error          | warn          |
| `zuplo:security` | error          | error         |

```txt
27:14  error  Routes must have an authentication policy: get /health  zuplo/require-policy
9:14   warn   Routes should have a rate limiting policy: get /users  zuplo/require-policy-warn
```

Requirements at warning severity report as `zuplo/require-policy-warn`.
Configure both through `zuplo/require-policy`: setting it in `rules` or
`overrides` applies to every requirement. `zuplo/require-policy-warn` isn't a
rule ID you can configure.

**Fix:** Add an authentication policy, such as
[API key authentication](../policies/api-key-inbound.mdx), or a
[rate limiting policy](../policies/rate-limit-inbound.mdx) to the route's
`policies.inbound` list. For routes that are intentionally public, such as a
health check, turn the rule off for them with an
[override](./api-lint-configuration.mdx#overrides):

```jsonc
{
  "version": 2,
  "overrides": [
    { "paths": ["/health"], "rules": { "zuplo/require-policy": "off" } },
  ],
}
```

To change which policy types count, set your own
[`requiredPolicies`](./api-lint-configuration.mdx#requiredpolicies). To require
a policy on only some routes, use a
[custom rule](./api-lint-custom-rules.mdx#require-a-policy-on-some-routes).

### `zuplo/require-cors-policy`

_`zuplo:strict` only._ Every route must set `corsPolicy` in `x-zuplo-route`, so
the CORS behavior of each route is a deliberate choice.

```txt
62:14  error  Route get /no-cors should have a corsPolicy in x-zuplo-route  zuplo/require-cors-policy
```

**Fix:** Set `corsPolicy` to the name of a [custom CORS policy](./cors.mdx), to
`"anything-goes"`, or to `"none"`. Any value satisfies the rule, including
`"none"`.

## Schema rules

These rules validate your configuration against the same JSON schemas Zuplo uses
at build time. They're part of every preset.

### `zuplo/schema-policies`

Validates the structure of `config/policies.json`: each policy has a `name`,
`policyType`, and `handler`, and each name uses only lowercase letters, numbers,
and dashes.

```txt
4:7   error  Schema validation error at /policies/0/name: at '/policies/0/name': 'INVALID_NAME' does not match pattern '^[0-9a-z-]+$'  zuplo/schema-policies
15:6  error  Schema validation error at /policies/1: at '/policies/1': missing property 'handler'  zuplo/schema-policies
```

**Fix:** Correct the field the message points to. The path after
`Schema validation error at` is the location in the file, so `/policies/0/name`
is the `name` of the first policy.

### `zuplo/schema-policy-options`

Validates `handler.options` of each built-in policy against that policy's
options. This catches missing required options, wrong types, and values outside
an allowed list. The rule reports warnings by default, so these results don't
fail a run unless you set `failOn` to `"warn"` or raise the rule to `"error"`.

```txt
17:44  warn  Schema validation error at /policies/1/handler/options: at '/policies/1/handler/options': missing properties 'rateLimitBy', 'requestsAllowed', 'timeWindowMinutes'  zuplo/schema-policy-options
10:11  warn  Schema validation error at /policies/0/handler/options/rateLimitBy: at '/policies/0/handler/options/rateLimitBy': value must be one of 'user', 'ip', 'function', 'all'  zuplo/schema-policy-options
```

Custom code policies and policy types the linter doesn't recognize are skipped,
so they never produce results from this rule.

**Fix:** Compare the options with the policy's page in the
[policy catalog](../policies/overview.mdx).

### `zuplo/schema-x-zuplo-route`

Validates each `x-zuplo-route` block in your OpenAPI files: it must have a
`handler`, policy names must use valid characters, and it can't contain
properties Zuplo doesn't recognize.

```txt
12:26  error  Schema validation error at : at '': additional properties 'unknownProperty' not allowed  zuplo/schema-x-zuplo-route
30:26  error  Schema validation error at : at '': missing property 'handler'  zuplo/schema-x-zuplo-route
```

An empty location (`at ''`) means the problem is in the `x-zuplo-route` object
itself.

**Fix:** Remove or rename the unrecognized property, or add the missing one. A
misspelled property such as `corsPolicies` shows up here as an additional
property. See [x-zuplo-route](./openapi.mdx#x-zuplo-route) for the properties it
accepts.

## Next steps

- [Lint configuration](./api-lint-configuration.mdx) — change severities, pick a
  preset, and require policies.
- [Lint in CI/CD](./api-lint-ci-cd.mdx) — run these rules on every pull request.
