Linting

Lint Configuration

Applies to
Gatewayruntime for API, AI, and MCP gateways
Hosting
Managed EdgeManaged DedicatedSelf-Hosted

zuplo lint reads its settings from a zuplo.lint.jsonc file in the root of your project. The file is optional: without it, the linter runs the zuplo:recommended preset and fails only on errors.

Start the file with $schema and version:

JSONzuplo.lint.jsonc
{ "$schema": "https://cdn.zuplo.com/schemas/zuplo-lint.json", "version": 2, "extends": ["zuplo:recommended"], }

$schema gives your editor completion and inline errors for every setting on this page. "version" is the version of the configuration format; set it to 2.

The linter looks for zuplo.lint.jsonc first, then zuplo.lint.json. To use a file somewhere else, pass its path with --config. The path is relative to the directory you run the command from, not to --dir:

TerminalCode
npx zuplo lint --config ./lint/strict.jsonc

The file is JSON with comments (JSONC), so you can annotate why a rule is off or a path is skipped. Trailing commas are allowed.

Full example

JSONzuplo.lint.jsonc
{ "$schema": "https://cdn.zuplo.com/schemas/zuplo-lint.json", "version": 2, // Rulesets to start from, applied in order "extends": [ "zuplo:strict", ["spectral:owasp", "recommended"], "./lint/team-ruleset.json", ], "rules": { // A severity: "error", "warn", "info", "hint", or "off" "info-contact": "off", // A severity scoped to some routes "operation-description": { "severity": "error", "paths": ["/v1/**"], "excludePaths": ["/v1/internal/**"], }, // A custom rule: require an API key policy on /v1 routes "require-api-key-v1": { "message": "v1 routes need an API key", "severity": "error", "paths": ["/v1/**"], "given": "$", "then": { "function": "zuplo:requirePolicy", "functionOptions": { "policyTypes": ["api-key"] }, }, }, }, // Change rules for some routes or files; later entries win "overrides": [ { "paths": ["/health"], "rules": { "zuplo/require-policy": "off", "operation-tags": "off" }, }, { "files": ["config/legacy.oas.json"], "rules": { "operation-description": "warn" }, }, ], // The lowest severity that fails the run "failOn": "error", // Compare the OpenAPI files with a Git branch "breakingChanges": { "enabled": true, "baseline": "origin/main" }, }

How settings combine

Code
extends (left to right) → rules → overrides (in order) → --fail-on flag
  1. extends builds the starting set of rules. When two rulesets define the same rule, the later one wins.
  2. rules changes the severity of those rules, limits them to some routes, or adds new ones.
  3. overrides changes severities for the routes or files they match. When several overrides match a result, the last one wins.
  4. --fail-on on the command line replaces failOn for one run.

Severities

Every rule reports at one severity:

SeverityMeaning
"error"A defect. Fails the run under the default failOn.
"warn"A likely problem. Fails the run only when failOn is "warn".
"info"Worth knowing. Reported, counted under info in the summary, never fails.
"hint"A suggestion. Reported like info, never fails.
"off"The rule doesn't run, or doesn't report on the routes it's off for.

Options

extends

The rulesets to start from. The default is ["zuplo:recommended"]. An entry is one of these:

A Zuplo preset. Each preset combines Zuplo's own checks with a reviewed set of OpenAPI rules that catch real defects without flooding a typical project.

PresetAdds
zuplo:recommendedThe core checks: operationId, descriptions, tags, path parameters, undefined policies, route labels, policy schemas, duplicate operations, unsafe Markdown, and more.
zuplo:strictEverything in recommended, plus required authentication and rate limiting, CORS on every route, API info, error responses, and documentation completeness.
zuplo:securityEverything in recommended, plus required authentication and rate limiting, both as errors.

A vacuum or Spectral built-in ruleset. These bring in the full catalog of general OpenAPI rules:

RulesetContains
vacuum:oas or spectral:oasGeneral OpenAPI rules.
vacuum:owasp or spectral:owaspOWASP API security rules.

On their own, these sets turn on that set's recommended rules. Write the entry as a pair to pick a mode instead, as in Spectral:

JSONCode
{ "version": 2, "extends": [ "zuplo:recommended", ["spectral:owasp", "all"], // every OWASP rule ], }
ModeEffect
"recommended"The set's recommended rules. The default.
"all"Every rule in the set.
"off"Removes the set's rules that an earlier entry added.

Some general OpenAPI rules don't fit Zuplo projects. For example, oas3-api-servers reports every spec because Zuplo supplies the servers, and paths-kebab-case reports URLPattern paths such as /users/:id. Turn those off in rules when you extend vacuum:oas, or start from a Zuplo preset, which leaves them out.

A local ruleset file. A .json or .jsonc file in the Spectral ruleset format, with a path relative to zuplo.lint.jsonc. Use it to share rules between projects. See Custom lint rules.

Run npx zuplo lint --list-rules to see every rule and which rulesets include it. The rules reference describes the rules in the Zuplo presets.

rules

Rule settings, keyed by rule ID. A value is either a severity or an object.

A severity changes the rule everywhere:

JSONCode
{ "version": 2, "rules": { "operation-tags": "off", "operation-description": "error", }, }

An object changes the severity and limits which routes the rule reports on:

JSONCode
{ "version": 2, "rules": { "operation-description": { "severity": "error", "paths": ["/v1/**"], "excludePaths": ["/v1/internal/**"], }, }, }
FieldDescription
severityThe rule's severity. Leave it out to keep the severity the rule already has.
pathsReport the rule only on these API paths.
excludePathsDon't report the rule on these API paths.

Path patterns work the same everywhere in this file:

  • A plain path matches that route exactly, as written in the OpenAPI file: "/users/{userId}".
  • A path ending in /** matches that path and every path beneath it. "/internal/**" matches /internal and /internal/jobs, but not /internal2.

A rule with paths doesn't report results that aren't tied to a route, such as a missing info.description or a policies.json schema error.

An object with given and then defines a new rule. See Custom lint rules.

A rule ID that no ruleset in extends defines stops the run with exit code 2, and the error suggests the closest ID. To configure a rule, extend a ruleset that includes it: "zuplo/require-cors-policy" is only available when you extend zuplo:strict.

zuplo/require-policy is the rule that enforces requiredPolicies. Setting it applies the severity to every required policy, and "off" turns all of them off. Use the base ID here; zuplo/require-policy-warn, which appears in results, isn't a rule you configure.

overrides

Change rule severities for some routes or OpenAPI files. Use an override when a group of routes is deliberately different from the rest of the API: public endpoints, a health check, a legacy spec you can't change yet.

JSONCode
{ "version": 2, "overrides": [ { "paths": ["/health", "/status"], "rules": { "zuplo/require-policy": "off" }, }, { "files": ["config/legacy.oas.json"], "rules": { "operation-description": "off", "operation-tags": "off", }, }, ], }
FieldDescription
pathsAPI paths the override applies to.
filesOpenAPI files the override applies to, relative to the project directory. * matches within one folder: config/*.oas.json.
rulesSeverities to apply, keyed by rule ID.

An override needs paths, files, or both. With both, a result must match both.

An override can also turn a rule on. With "operation-tags": "off" in rules and { "paths": ["/v1/**"], "rules": { "operation-tags": "warn" } } in overrides, the rule reports only on /v1 routes. An override works within a rule's own paths and excludePaths, and can't turn a rule on for a path in ignorePaths.

failOn

The lowest severity that fails the run with exit code 1.

ValueThe run fails when
"error"There is at least one error, or a breaking change reported as an error.
"warn"There is at least one error, warning, or breaking change.
"none"Never. The linter reports results and exits with 0.

The default is "error". info and hint results never fail a run. Any other value stops the run with exit code 2. The --fail-on flag overrides this value for a single run.

ignorePaths

API paths whose route results the linter drops for every rule. An override can't turn a rule back on for an ignored path.

JSONCode
{ "version": 2, "ignorePaths": ["/health", "/internal/**"], }

ignorePaths also applies to automatic fixes. Results that aren't tied to a route still appear: policies.json schema results, info rules, and breaking changes. To turn off only some rules for a route, use overrides.

include

The OpenAPI files to lint, relative to the project directory. By default, the linter lints every file directly in config/ that ends in .oas.json; it doesn't look in subdirectories.

JSONCode
{ "version": 2, "include": ["config/public-api.oas.json"], }

Every entry must end in .oas.json, must be a relative path, and must stay inside the project directory. Glob patterns aren't supported, so list each file. A file that doesn't exist fails the run with exit code 2.

The CLI still requires at least one .oas.json file directly in config/, so use include to narrow the files that are linted, not to point at files elsewhere.

include controls which OpenAPI files are linted. The linter always validates config/policies.json when it exists.

requiredPolicies

Policies that every route must include. The zuplo:strict and zuplo:security presets each define a default set; requiredPolicies replaces those defaults with your own list.

JSONCode
{ "version": 2, "extends": ["zuplo:recommended"], "requiredPolicies": [ { "id": "require-auth", "policyTypes": ["api-key", "open-id-jwt-auth"], "direction": "inbound", "severity": "error", "message": "Routes must have an authentication policy", }, { "id": "require-rate-limit", "policyTypes": ["rate-limit"], "severity": "warn", "message": "Routes should have a rate limiting policy", }, ], }

Unlike other rules, requiredPolicies works with any preset, including zuplo:recommended. Each requirement applies to every route; to require a policy on only some routes, use overrides or a custom rule with paths.

Each entry has these fields:

FieldDescription
idA name for the requirement. Use it to keep track of entries; results report under zuplo/require-policy.
policyTypesPolicy types that satisfy the requirement. A route needs at least one policy whose type matches.
direction"inbound" or "outbound". Defaults to "inbound".
severity"error" or "warn". Defaults to "error".
messageThe message to report. The linter appends the method and path of the route, for example : get /todos.

The linter matches each policy name in the route's x-zuplo-route.policies list to its policyType in config/policies.json. An entry in policyTypes matches that exact type and any type that starts with it followed by a dash, so "api-key" matches api-key-inbound, and "rate-limit" matches rate-limit-inbound. Matching starts at the beginning of the type, so "rate-limit" doesn't match complex-rate-limit-inbound; list "complex-rate-limit" separately if you use it.

Requirements with "severity": "error" report under the rule ID zuplo/require-policy. Requirements with "severity": "warn" report under zuplo/require-policy-warn.

To turn off the preset defaults without adding your own requirements, set an empty list:

JSONCode
{ "version": 2, "extends": ["zuplo:strict"], "requiredPolicies": [], }

The defaults in zuplo:strict and zuplo:security accept these policy types:

  • Authentication: api-key, auth0-jwt-auth, clerk-jwt-auth, cognito-jwt-auth, curity-phantom-token, firebase-jwt-auth, jwt-auth, oauth2-pkce-auth, okta-jwt-auth, open-id-jwt-auth, propel-auth, stytch-jwt-auth, supabase-jwt-auth
  • Rate limiting: rate-limit, complex-rate-limit

breakingChanges

Compares each OpenAPI file with the same file on a Git branch, tag, or commit, and reports changes that can break existing API consumers, such as a removed endpoint or a changed type.

JSONCode
{ "version": 2, "breakingChanges": { "enabled": true, "baseline": "origin/main", "severity": "error", }, }
FieldDescription
enabledTurns detection on. Defaults to false.
baselineThe branch, tag, or commit SHA to compare against. Defaults to "main".
severity"error" or "warn". Defaults to "error".

See Breaking change detection for how to choose a baseline, what counts as a breaking change, and how to run it in CI.

Command-line flags and the configuration file

Flags take precedence over the configuration file for a single run:

FlagOverrides
--configWhich configuration file to read.
--fail-onfailOn.

All other settings come only from the configuration file. See the zuplo lint CLI reference for every flag.

Next steps

Last modified on