Linting

Custom Lint Rules

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

When the built-in rules don't cover a convention your team follows, add your own in zuplo.lint.jsonc. Custom rules use the Spectral ruleset format, so a rule written for Spectral or vacuum works here unchanged.

Write a rule

A rule selects parts of your OpenAPI document with given and checks each one with a function in then:

JSONzuplo.lint.jsonc
{ "$schema": "https://cdn.zuplo.com/schemas/zuplo-lint.json", "version": 2, "extends": ["zuplo:recommended"], "rules": { "operation-summary-length": { "description": "Summaries must fit in the developer portal sidebar", "message": "Keep summaries to 60 characters or fewer", "severity": "warn", "given": "$.paths[*][*].summary", "then": { "function": "length", "functionOptions": { "max": 60 } }, }, }, }
Code
config/routes.oas.json 45:20 warn Keep summaries to 60 characters or fewer: 'List every todo item that belongs to the current user, sorted by the date it was created' must not be longer/greater than '60' operation-summary-length $.paths['/v1/todos'].get.summary

The key, operation-summary-length, is the rule ID. It appears in results, and you use it to change the rule's severity in overrides. IDs starting with zuplo/ are reserved for Zuplo's rules.

FieldDescription
givenA JSONPath expression, or a list of them, selecting the values to check.
thenThe function to run on each selected value, or a list of functions. Each has function, and optionally field and functionOptions.
severity"error", "warn", "info", "hint", or "off". Defaults to "warn".
messageThe text each result starts with. Defaults to description, then to the rule ID.
descriptionWhat the rule checks. Used as the message when message isn't set.
pathsReport the rule only on these API paths, such as ["/v1/**"].
excludePathsDon't report the rule on these API paths.
formatsDocument formats the rule applies to, such as ["oas3"].

then.field checks a property of each selected value instead of the value itself, so this rule reports every operation without a summary:

JSONCode
{ "version": 2, "rules": { "operation-summary": { "description": "Operations need a summary", "given": "$.paths[*][*]", "then": { "field": "summary", "function": "truthy" }, }, }, }

Functions

These are the general-purpose functions, the same as Spectral's core functions:

FunctionReports when the value...Options
truthyis missing, empty, false, or 0
falsyis set to a truthy value
definedis missing
undefinedis set
patterndoesn't match match, or matches notMatchmatch, notMatch (regular expressions)
casingisn't in the given casetype: camel, pascal, kebab, snake, cobol, flat, macro
lengthis shorter than min or longer than max (characters, items, or keys)min, max
enumerationisn't one of valuesvalues
alphabeticalisn't sortedkeyedBy for lists of objects
schemadoesn't match a JSON schemaschema
xorhas more or fewer than one of propertiesproperties

A Zuplo route has settings, such as its policies, that the OpenAPI document alone can't explain, because policies are defined in config/policies.json. These Zuplo functions read both files:

FunctionReports a route that...Options
zuplo:requirePolicydoesn't have a policy of one of the given typespolicyTypes (required), direction (inbound or outbound)
zuplo:noUndefinedPoliciesreferences a policy that isn't in config/policies.json
zuplo:noEmptyInboundPolicieshas an empty policies.inbound list
zuplo:requireRouteLabelhas no label in x-zuplo-route
zuplo:requireCorsPolicyhas no corsPolicy in x-zuplo-route

A Zuplo function checks every route itself, so set "given": "$" and choose the routes with paths and excludePaths. Only zuplo:requirePolicy uses your message; the other Zuplo functions write their own.

Run npx zuplo lint --list-rules to see every function and rule available.

Examples

Require a policy on some routes

requiredPolicies applies to every route. A custom rule with zuplo:requirePolicy applies to the routes you choose:

JSONCode
{ "version": 2, "rules": { "require-api-key-v1": { "message": "v1 routes need an API key", "severity": "error", "paths": ["/v1/**"], "given": "$", "then": { "function": "zuplo:requirePolicy", "functionOptions": { "policyTypes": ["api-key"] }, }, }, }, }
Code
16:14 error v1 routes need an API key: get /v1/admin require-api-key-v1 paths./v1/admin.get.x-zuplo-route.policies.inbound

policyTypes matches the same way as in requiredPolicies: "api-key" matches the api-key-inbound policy type. Add one rule per requirement, each with its own ID and paths.

Enforce a naming convention

JSONCode
{ "version": 2, "rules": { "operation-id-camel-case": { "description": "operationIds use camelCase", "severity": "error", "given": "$.paths[*][*].operationId", "then": { "function": "casing", "functionOptions": { "type": "camel" } }, }, }, }

Keep internal headers out of the public API

JSONCode
{ "version": 2, "rules": { "no-x-internal-headers": { "description": "Don't expose X-Internal-* headers", "severity": "warn", "given": "$.paths[*][*].parameters[?(@.in == 'header')].name", "then": { "function": "pattern", "functionOptions": { "notMatch": "^X-Internal-" }, }, }, }, }

Share rules with a ruleset file

To use the same rules in several projects, put them in a ruleset file and extend it. A ruleset file is a .json or .jsonc file in the Spectral ruleset format:

JSONlint/team-ruleset.jsonc
{ // Rules shared by every API team "rules": { "operation-id-camel-case": { "description": "operationIds use camelCase", "severity": "error", "given": "$.paths[*][*].operationId", "then": { "function": "casing", "functionOptions": { "type": "camel" } }, }, }, }
JSONzuplo.lint.jsonc
{ "$schema": "https://cdn.zuplo.com/schemas/zuplo-lint.json", "version": 2, "extends": ["zuplo:recommended", "./lint/team-ruleset.jsonc"], }

The path is relative to zuplo.lint.jsonc. A ruleset file can extend the built-in rulesets (vacuum:oas, spectral:oas, vacuum:owasp, spectral:owasp) but not other files. Rulesets must be local files: the linter doesn't download rulesets from a URL, so copy a shared ruleset into your repository, or add it as a package, and reference that path. YAML rulesets aren't supported; convert them to JSON.

zuplo.lint.jsonc can still change the severity or scope of a rule from a ruleset file, the same as any other rule.

Next steps

Last modified on