zuplo 7.13.3 → 7.13.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,360 @@
1
+ ---
2
+ title: Lint Rules Reference
3
+ sidebar_label: Rules reference
4
+ description:
5
+ Every rule `zuplo lint` runs, which presets turn it on, its default severity,
6
+ example output, and how to fix the OpenAPI file or policy that triggers it.
7
+ applies_to:
8
+ product: gateway
9
+ hosting: all
10
+ ---
11
+
12
+ Each rule has an ID, a default severity, and the presets that include it. Use
13
+ the ID to change the rule's severity, limit it to some routes, or turn it off in
14
+ [`zuplo.lint.jsonc`](./api-lint-configuration.mdx#rules). To add rules of your
15
+ own, see [Custom lint rules](./api-lint-custom-rules.mdx).
16
+
17
+ ## Rules by preset
18
+
19
+ | Rule | Severity | recommended | strict | security | Auto-fix |
20
+ | -------------------------------------------------------------------- | ---------- | :---------: | :----: | :------: | :------: |
21
+ | [`operation-operationId`](#operation-operationid) | error | ✓ | ✓ | ✓ | ✓ |
22
+ | [`operation-description`](#operation-description) | warn | ✓ | ✓ | ✓ | |
23
+ | [`operation-tags`](#operation-tags) | warn | ✓ | ✓ | ✓ | |
24
+ | [`path-params`](#path-params) | error | ✓ | ✓ | ✓ | |
25
+ | [`no-ambiguous-paths`](#no-ambiguous-paths) | error | ✓ | ✓ | ✓ | |
26
+ | [`zuplo/no-undefined-policies`](#zuplono-undefined-policies) | error | ✓ | ✓ | ✓ | |
27
+ | [`zuplo/no-empty-inbound-policies`](#zuplono-empty-inbound-policies) | warn | ✓ | ✓ | ✓ | ✓ |
28
+ | [`zuplo/require-route-label`](#zuplorequire-route-label) | warn | ✓ | ✓ | ✓ | |
29
+ | [`zuplo/schema-policies`](#zuploschema-policies) | error | ✓ | ✓ | ✓ | |
30
+ | [`zuplo/schema-policy-options`](#zuploschema-policy-options) | warn | ✓ | ✓ | ✓ | |
31
+ | [`zuplo/schema-x-zuplo-route`](#zuploschema-x-zuplo-route) | error | ✓ | ✓ | ✓ | |
32
+ | [`zuplo/require-policy`](#zuplorequire-policy) | per policy | | ✓ | ✓ | |
33
+ | [`zuplo/require-cors-policy`](#zuplorequire-cors-policy) | error | | ✓ | | |
34
+ | [`info-description`](#info-description) | warn | | ✓ | | |
35
+ | [`info-contact`](#info-contact) | warn | | ✓ | | |
36
+ | [`no-http-verbs-in-path`](#no-http-verbs-in-path) | error | | ✓ | | |
37
+
38
+ Two more IDs can appear in results without being rules you configure:
39
+
40
+ - `breaking-change` — reported by
41
+ [breaking change detection](./api-lint-breaking-changes.mdx).
42
+ - `lint-execution-error` — a rule failed to run against a file. This usually
43
+ means the file isn't valid JSON or isn't valid OpenAPI, so it has no line
44
+ number (`0:0`). Fix the file and run the linter again.
45
+
46
+ ## General OpenAPI rules
47
+
48
+ The Zuplo presets also include these general OpenAPI rules. Each one catches a
49
+ real defect and stays quiet on a typical Zuplo project.
50
+
51
+ | Rule | Severity | Presets | Reports |
52
+ | ------------------------------------ | -------- | ------- | ----------------------------------------------------------------- |
53
+ | `duplicate-paths` | error | all | The same path defined twice; only the last definition is kept. |
54
+ | `operation-operationId-unique` | error | all | Two operations with the same `operationId`. |
55
+ | `operation-parameters` | error | all | The same parameter declared twice on an operation. |
56
+ | `path-declarations-must-exist` | error | all | An empty path parameter, such as `/users/{}`. |
57
+ | `path-not-include-query` | error | all | A query string in a path key. |
58
+ | `oas3-operation-security-defined` | error | all | A `security` entry with no matching `securitySchemes` definition. |
59
+ | `oas-schema-check` | error | all | A schema with an invalid `type`, such as `"string "`. |
60
+ | `no-eval-in-markdown` | error | all | `eval()` in a Markdown description. |
61
+ | `no-script-tags-in-markdown` | error | all | `<script>` in a Markdown description. |
62
+ | `operation-operationId-valid-in-url` | warn | all | An `operationId` with characters that aren't URL-friendly. |
63
+ | `operation-success-response` | warn | all | An operation without a `2xx` or `3xx` response. |
64
+ | `oas3-valid-schema-example` | warn | all | An example that doesn't match its schema. |
65
+ | `typed-enum` | warn | all | An enum value that doesn't match the enum's type. |
66
+ | `duplicated-entry-in-enum` | warn | all | A repeated enum value. |
67
+ | `nullable-enum-contains-null` | warn | all | A nullable enum without `null` in its values. |
68
+ | `path-keys-no-trailing-slash` | warn | all | A path ending in `/`. |
69
+ | `no-request-body` | warn | all | A request body on a `GET` or `DELETE` operation. |
70
+ | `operation-4xx-response` | warn | strict | An operation without a `4xx` error response. |
71
+ | `oas3-parameter-description` | warn | strict | A parameter without a description. |
72
+ | `component-description` | warn | strict | A component without a description. |
73
+ | `operation-tag-defined` | warn | strict | An operation tag missing from the top-level `tags` list. |
74
+ | `oas3-unused-component` | warn | strict | A component nothing references. |
75
+ | `required-fields-defined` | warn | strict | A `required` property that isn't declared in `properties`. |
76
+ | `no-unnecessary-combinator` | warn | strict | An `allOf`, `anyOf`, or `oneOf` with a single item. |
77
+
78
+ These rules come from vacuum, the engine behind `zuplo lint`. To use vacuum's
79
+ other general OpenAPI rules, or its OWASP security rules, extend `vacuum:oas` or
80
+ `spectral:owasp`; see [`extends`](./api-lint-configuration.mdx#extends). Run
81
+ `npx zuplo lint --list-rules` to see every rule and which rulesets include it.
82
+
83
+ ## OpenAPI rules
84
+
85
+ These rules check the OpenAPI document itself. They apply to every operation,
86
+ whether or not it has an `x-zuplo-route` block.
87
+
88
+ ### `operation-operationId`
89
+
90
+ Every operation must have an `operationId`. Zuplo uses it to identify the route,
91
+ and code generators and the Developer Portal use it to name operations.
92
+
93
+ ```txt
94
+ 6:14 error Every operation must have an operationId: `operationId` must be set operation-operationId
95
+ ```
96
+
97
+ **Fix:** Add a unique `operationId` to the operation, or run `zuplo lint --fix`
98
+ to generate one from the method and path:
99
+
100
+ ```json
101
+ "/todos/{id}": {
102
+ "delete": {
103
+ "operationId": "deleteTodosById",
104
+ ```
105
+
106
+ ### `operation-description`
107
+
108
+ Operations should have a `description`. The Developer Portal shows it as the
109
+ operation's documentation.
110
+
111
+ ```txt
112
+ 6:14 warn Operations should have descriptions: `description` must be set operation-description
113
+ ```
114
+
115
+ **Fix:** Add a `description` that explains what the operation does.
116
+
117
+ ### `operation-tags`
118
+
119
+ Operations should have at least one tag. The Developer Portal groups operations
120
+ by tag.
121
+
122
+ ```txt
123
+ 6:14 warn Operations should have tags: `tags` must be set operation-tags
124
+ ```
125
+
126
+ **Fix:** Add a `tags` array, for example `"tags": ["Todos"]`.
127
+
128
+ ### `path-params`
129
+
130
+ Every parameter in a path template must be declared, either on the path or on
131
+ the operation. The rule also reports two paths that differ only in the names of
132
+ their parameters.
133
+
134
+ ```txt
135
+ 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
136
+ ```
137
+
138
+ **Fix:** Declare the parameter with `"in": "path"` and `"required": true`:
139
+
140
+ ```json
141
+ "/todos/{id}": {
142
+ "delete": {
143
+ "parameters": [
144
+ {
145
+ "name": "id",
146
+ "in": "path",
147
+ "required": true,
148
+ "schema": { "type": "string" }
149
+ }
150
+ ],
151
+ ```
152
+
153
+ ### `no-ambiguous-paths`
154
+
155
+ Two paths can't match the same request. `/users/{id}` and `/users/{name}` are
156
+ ambiguous: a request to `/users/42` matches both.
157
+
158
+ ```txt
159
+ 6:5 error paths are ambiguous with one another: `/users/{id}` (GET) and `/users/{name}` (GET) no-ambiguous-paths
160
+ ```
161
+
162
+ This pair is also reported by [`path-params`](#path-params), so turning off one
163
+ of the two rules doesn't silence it.
164
+
165
+ **Fix:** Merge the two paths into one, or change one so it no longer overlaps,
166
+ for example `/users/by-name/{name}`.
167
+
168
+ ### `info-description`
169
+
170
+ _`zuplo:strict` only._ The document's `info` object must have a `description`.
171
+
172
+ ```txt
173
+ 3:11 warn API info must have a description: `description` must be set info-description
174
+ ```
175
+
176
+ **Fix:** Add `info.description`.
177
+
178
+ ### `info-contact`
179
+
180
+ _`zuplo:strict` only._ The document's `info` object should have a `contact`.
181
+
182
+ ```txt
183
+ 3:11 warn API info should have contact info: `contact` must be set info-contact
184
+ ```
185
+
186
+ **Fix:** Add `info.contact`, for example
187
+ `"contact": { "name": "API Support", "email": "api@example.com" }`.
188
+
189
+ ### `no-http-verbs-in-path`
190
+
191
+ _`zuplo:strict` only._ Path segments shouldn't be HTTP methods. The HTTP method
192
+ already says what the operation does.
193
+
194
+ ```txt
195
+ 7:19 error path `/users/get` contains an HTTP Verb `get` no-http-verbs-in-path
196
+ ```
197
+
198
+ The rule matches whole segments, so `/users/get` and `/post` are reported, while
199
+ `/getUsers` and `/delete-user` aren't.
200
+
201
+ **Fix:** Move the action into the method: `GET /users` instead of
202
+ `GET /users/get`.
203
+
204
+ ## Zuplo route rules
205
+
206
+ These rules check the `x-zuplo-route` block of each operation. Operations
207
+ without an `x-zuplo-route` block are skipped.
208
+
209
+ ### `zuplo/no-undefined-policies`
210
+
211
+ Every policy name in a route's `policies.inbound` or `policies.outbound` list
212
+ must be defined in `config/policies.json`. A missing policy fails the build.
213
+
214
+ ```txt
215
+ 15:56 error Policy "rate-limit-inbound" is referenced in route but not defined in policies.json zuplo/no-undefined-policies
216
+ ```
217
+
218
+ **Fix:** Correct the spelling of the policy name in the route, or add the policy
219
+ to `config/policies.json`. The rule only runs when `config/policies.json`
220
+ defines at least one policy.
221
+
222
+ ### `zuplo/no-empty-inbound-policies`
223
+
224
+ A route shouldn't have an empty `policies.inbound` array. An empty array has the
225
+ same effect as no array, and it's usually left behind after removing a policy.
226
+
227
+ ```txt
228
+ 31:36 warn Route delete /todos/{id} has an empty inbound policies array zuplo/no-empty-inbound-policies
229
+ ```
230
+
231
+ **Fix:** Remove the empty array, or run `zuplo lint --fix`.
232
+
233
+ ### `zuplo/require-route-label`
234
+
235
+ Routes should have a `label` in `x-zuplo-route`. The label names the route in
236
+ the Zuplo Portal's route designer, logs, and analytics.
237
+
238
+ ```txt
239
+ 6:14 warn Route get /todos should have a label in x-zuplo-route zuplo/require-route-label
240
+ ```
241
+
242
+ **Fix:** Add `"label": "List todos"` to the `x-zuplo-route` block.
243
+
244
+ ### `zuplo/require-policy`
245
+
246
+ _`zuplo:strict` and `zuplo:security`, or any preset with
247
+ [`requiredPolicies`](./api-lint-configuration.mdx#requiredpolicies)._ Every
248
+ route must include a policy of each required type.
249
+
250
+ | Preset | Authentication | Rate limiting |
251
+ | ---------------- | -------------- | ------------- |
252
+ | `zuplo:strict` | error | warn |
253
+ | `zuplo:security` | error | error |
254
+
255
+ ```txt
256
+ 27:14 error Routes must have an authentication policy: get /health zuplo/require-policy
257
+ 9:14 warn Routes should have a rate limiting policy: get /users zuplo/require-policy-warn
258
+ ```
259
+
260
+ Requirements at warning severity report as `zuplo/require-policy-warn`.
261
+ Configure both through `zuplo/require-policy`: setting it in `rules` or
262
+ `overrides` applies to every requirement. `zuplo/require-policy-warn` isn't a
263
+ rule ID you can configure.
264
+
265
+ **Fix:** Add an authentication policy, such as
266
+ [API key authentication](../policies/api-key-inbound.mdx), or a
267
+ [rate limiting policy](../policies/rate-limit-inbound.mdx) to the route's
268
+ `policies.inbound` list. For routes that are intentionally public, such as a
269
+ health check, turn the rule off for them with an
270
+ [override](./api-lint-configuration.mdx#overrides):
271
+
272
+ ```jsonc
273
+ {
274
+ "version": 2,
275
+ "overrides": [
276
+ { "paths": ["/health"], "rules": { "zuplo/require-policy": "off" } },
277
+ ],
278
+ }
279
+ ```
280
+
281
+ To change which policy types count, set your own
282
+ [`requiredPolicies`](./api-lint-configuration.mdx#requiredpolicies). To require
283
+ a policy on only some routes, use a
284
+ [custom rule](./api-lint-custom-rules.mdx#require-a-policy-on-some-routes).
285
+
286
+ ### `zuplo/require-cors-policy`
287
+
288
+ _`zuplo:strict` only._ Every route must set `corsPolicy` in `x-zuplo-route`, so
289
+ the CORS behavior of each route is a deliberate choice.
290
+
291
+ ```txt
292
+ 62:14 error Route get /no-cors should have a corsPolicy in x-zuplo-route zuplo/require-cors-policy
293
+ ```
294
+
295
+ **Fix:** Set `corsPolicy` to the name of a [custom CORS policy](./cors.mdx), to
296
+ `"anything-goes"`, or to `"none"`. Any value satisfies the rule, including
297
+ `"none"`.
298
+
299
+ ## Schema rules
300
+
301
+ These rules validate your configuration against the same JSON schemas Zuplo uses
302
+ at build time. They're part of every preset.
303
+
304
+ ### `zuplo/schema-policies`
305
+
306
+ Validates the structure of `config/policies.json`: each policy has a `name`,
307
+ `policyType`, and `handler`, and each name uses only lowercase letters, numbers,
308
+ and dashes.
309
+
310
+ ```txt
311
+ 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
312
+ 15:6 error Schema validation error at /policies/1: at '/policies/1': missing property 'handler' zuplo/schema-policies
313
+ ```
314
+
315
+ **Fix:** Correct the field the message points to. The path after
316
+ `Schema validation error at` is the location in the file, so `/policies/0/name`
317
+ is the `name` of the first policy.
318
+
319
+ ### `zuplo/schema-policy-options`
320
+
321
+ Validates `handler.options` of each built-in policy against that policy's
322
+ options. This catches missing required options, wrong types, and values outside
323
+ an allowed list. The rule reports warnings by default, so these results don't
324
+ fail a run unless you set `failOn` to `"warn"` or raise the rule to `"error"`.
325
+
326
+ ```txt
327
+ 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
328
+ 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
329
+ ```
330
+
331
+ Custom code policies and policy types the linter doesn't recognize are skipped,
332
+ so they never produce results from this rule.
333
+
334
+ **Fix:** Compare the options with the policy's page in the
335
+ [policy catalog](../policies/overview.mdx).
336
+
337
+ ### `zuplo/schema-x-zuplo-route`
338
+
339
+ Validates each `x-zuplo-route` block in your OpenAPI files: it must have a
340
+ `handler`, policy names must use valid characters, and it can't contain
341
+ properties Zuplo doesn't recognize.
342
+
343
+ ```txt
344
+ 12:26 error Schema validation error at : at '': additional properties 'unknownProperty' not allowed zuplo/schema-x-zuplo-route
345
+ 30:26 error Schema validation error at : at '': missing property 'handler' zuplo/schema-x-zuplo-route
346
+ ```
347
+
348
+ An empty location (`at ''`) means the problem is in the `x-zuplo-route` object
349
+ itself.
350
+
351
+ **Fix:** Remove or rename the unrecognized property, or add the missing one. A
352
+ misspelled property such as `corsPolicies` shows up here as an additional
353
+ property. See [x-zuplo-route](./openapi.mdx#x-zuplo-route) for the properties it
354
+ accepts.
355
+
356
+ ## Next steps
357
+
358
+ - [Lint configuration](./api-lint-configuration.mdx) — change severities, pick a
359
+ preset, and require policies.
360
+ - [Lint in CI/CD](./api-lint-ci-cd.mdx) — run these rules on every pull request.
@@ -0,0 +1,267 @@
1
+ ---
2
+ title: Linting Your API
3
+ sidebar_label: Overview
4
+ description:
5
+ Use `zuplo lint` to check OpenAPI files and policies for missing fields,
6
+ undefined policies, invalid options, and breaking changes before you deploy a
7
+ gateway.
8
+ applies_to:
9
+ product: gateway
10
+ hosting: all
11
+ ---
12
+
13
+ A Zuplo gateway is mostly configuration: OpenAPI files in `config/` describe
14
+ your routes, and `config/policies.json` defines the policies those routes run. A
15
+ typo in that configuration — a policy name that doesn't exist, a required option
16
+ left out, a route with no authentication — doesn't fail a type check. It shows
17
+ up later, as a failed build or a route that behaves differently than you expect.
18
+
19
+ `zuplo lint` catches those problems on your machine or in CI, before you deploy.
20
+ It reads the same files the gateway reads and reports each problem with the
21
+ file, line, and column where it occurs.
22
+
23
+ ```bash
24
+ npx zuplo lint
25
+ ```
26
+
27
+ ## What the linter checks
28
+
29
+ The linter runs three kinds of checks against every `*.oas.json` file in your
30
+ project's `config/` directory and against `config/policies.json`:
31
+
32
+ - **OpenAPI quality** — every operation has an `operationId`, a description, and
33
+ tags; path parameters are declared; no two paths are ambiguous.
34
+ - **Zuplo route configuration** — every policy a route references exists in
35
+ `policies.json`, `x-zuplo-route` blocks contain only valid properties, and
36
+ routes have a label. The stricter presets also require authentication and rate
37
+ limiting on every route, and `zuplo:strict` requires a CORS setting.
38
+ - **Policy configuration** — `policies.json` has the right structure, and each
39
+ built-in policy's `handler.options` match that policy's schema, so a missing
40
+ `requestsAllowed` on a rate limit policy or a misspelled enum value shows up
41
+ as a warning before you deploy.
42
+
43
+ Optionally, the linter also compares your OpenAPI files against a Git branch and
44
+ reports [breaking changes](./api-lint-breaking-changes.mdx), such as a removed
45
+ endpoint.
46
+
47
+ The full list of checks, with an example of each, is in the
48
+ [rules reference](./api-lint-rules.mdx).
49
+
50
+ ## Run the linter
51
+
52
+ Run the command from the root of your Zuplo project — the directory that
53
+ contains `config/`:
54
+
55
+ ```bash
56
+ npx zuplo lint
57
+ ```
58
+
59
+ The linter prints each result grouped by file, followed by a summary:
60
+
61
+ ```txt
62
+ Zuplo API Lint
63
+ Config: zuplo:recommended | failOn: error | source: default
64
+
65
+ config/routes.oas.json
66
+ 6:14 warn Operations should have descriptions: `description` must be set operation-description $.paths['/todos'].get.description
67
+ 6:14 error Every operation must have an operationId: `operationId` must be set operation-operationId $.paths['/todos'].get.operationId
68
+ 6:14 warn Operations should have tags: `tags` must be set operation-tags $.paths['/todos'].get.tags
69
+ 6:14 warn Route get /todos should have a label in x-zuplo-route zuplo/require-route-label paths./todos.get.x-zuplo-route.label
70
+ 15:56 error Policy "rate-limit-inbound" is referenced in route but not defined in policies.json zuplo/no-undefined-policies paths./todos.get.x-zuplo-route.policies.inbound[1]
71
+ 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 $.paths['/todos/{id}']
72
+ 20:17 error Every operation must have an operationId: `operationId` must be set operation-operationId $.paths['/todos/{id}'].delete.operationId
73
+ 20:17 warn Route delete /todos/{id} should have a label in x-zuplo-route zuplo/require-route-label paths./todos/{id}.delete.x-zuplo-route.label
74
+ 31:36 warn Route delete /todos/{id} has an empty inbound policies array zuplo/no-empty-inbound-policies paths./todos/{id}.delete.x-zuplo-route.policies.inbound
75
+
76
+ ✖ 4 errors, 5 warnings remaining
77
+ ```
78
+
79
+ Each line shows the `line:column` of the problem, its severity, a message, the
80
+ ID of the rule that reported it, and the location inside the document. Use the
81
+ rule ID to look the rule up in the [rules reference](./api-lint-rules.mdx) or to
82
+ change its severity in your [configuration](./api-lint-configuration.mdx#rules).
83
+
84
+ The header shows which presets ran, the severity that fails the run, and where
85
+ the configuration came from: `default` when there is no configuration file,
86
+ `file (auto-detected)` when the linter found a `zuplo.lint.jsonc`, or
87
+ `file (--config)` when you passed one.
88
+
89
+ When nothing is left to report, the run ends with `No violations found.` and
90
+ exits with `0`.
91
+
92
+ To lint a project in another directory, pass `--dir`:
93
+
94
+ ```bash
95
+ npx zuplo lint --dir ./apps/gateway
96
+ ```
97
+
98
+ ## Errors, warnings, and the exit code
99
+
100
+ Every rule reports at a severity of `error` or `warn`. By default, the command
101
+ fails only when it finds at least one error, so warnings point out improvements
102
+ without blocking you.
103
+
104
+ | Exit code | Meaning |
105
+ | --------- | --------------------------------------------------------------------------------------------------- |
106
+ | `0` | No results at or above the failure threshold. |
107
+ | `1` | The linter found results at or above the failure threshold. |
108
+ | `2` | The linter couldn't run: the directory isn't a Zuplo project, or the configuration file is invalid. |
109
+
110
+ Change the threshold with `--fail-on`:
111
+
112
+ ```bash
113
+ # Fail on warnings as well as errors
114
+ npx zuplo lint --fail-on warn
115
+
116
+ # Report everything but never fail
117
+ npx zuplo lint --fail-on none
118
+ ```
119
+
120
+ To set the threshold for everyone on the project, set
121
+ [`failOn`](./api-lint-configuration.mdx#failon) in the configuration file
122
+ instead. The `--fail-on` flag overrides it.
123
+
124
+ ## Fix problems automatically
125
+
126
+ Some problems have exactly one correct fix. Run `--fix` to apply those fixes to
127
+ your OpenAPI files, then lint the result:
128
+
129
+ ```bash
130
+ npx zuplo lint --fix
131
+ ```
132
+
133
+ ```txt
134
+ Fixed
135
+
136
+ config/routes.oas.json
137
+ ✔ Added operationId "getTodos" to GET /todos operation-operationId
138
+ ✔ Added operationId "deleteTodosById" to DELETE /todos/{id} operation-operationId
139
+ ✔ Removed empty inbound policies array from DELETE /todos/{id} zuplo/no-empty-inbound-policies
140
+ ```
141
+
142
+ The results printed after the fixes are the problems that still need your
143
+ attention.
144
+
145
+ `--fix` repairs two rules:
146
+
147
+ | Rule | Fix |
148
+ | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
149
+ | `operation-operationId` | Adds an `operationId` built from the method and path. `DELETE /todos/{id}` becomes `deleteTodosById`. |
150
+ | `zuplo/no-empty-inbound-policies` | Removes an empty `policies.inbound` array, and the `policies` object if nothing else is left in it. |
151
+
152
+ Everything else — descriptions, tags, labels, missing policies, path parameters,
153
+ authentication — needs a decision only you can make, so the linter reports it
154
+ and leaves the file alone.
155
+
156
+ Automatic fixes are safe to run repeatedly:
157
+
158
+ - **Formatting is preserved.** The linter edits only what it fixes (plus a
159
+ trailing comma when it removes a property), so the change shows up as a small
160
+ diff rather than a reformatted file. Files with nothing to fix aren't written.
161
+ If an OpenAPI file isn't valid JSON, the linter changes nothing and exits with
162
+ `2`.
163
+ - **Generated `operationId` values are unique across the whole project**, not
164
+ only within one file. If the name is already taken, the linter adds a number:
165
+ `getTodos2`.
166
+ - **Your configuration is respected.** A rule that's off for a route, through
167
+ `rules`, [`overrides`](./api-lint-configuration.mdx#overrides), or
168
+ [`ignorePaths`](./api-lint-configuration.mdx#ignorepaths), isn't fixed on that
169
+ route.
170
+ - **Running it twice changes nothing the second time.**
171
+
172
+ :::tip
173
+
174
+ Commit or stash your changes before running `--fix`, so the fixes show up as
175
+ their own diff and are easy to review.
176
+
177
+ :::
178
+
179
+ ## Output formats
180
+
181
+ The default `text` output is for reading in a terminal. Two other formats are
182
+ for tools:
183
+
184
+ | Format | Use it for |
185
+ | ------- | ------------------------------------------------------------------------------------------- |
186
+ | `text` | Reading results in a terminal. Set `NO_COLOR=1` to turn off colors. |
187
+ | `json` | Scripts and custom CI steps. Includes a summary, every result, breaking changes, and fixes. |
188
+ | `sarif` | GitHub code scanning and other tools that read SARIF 2.1.0. |
189
+
190
+ ```bash
191
+ npx zuplo lint --format json > lint-results.json
192
+ npx zuplo lint --format sarif > lint-results.sarif
193
+ ```
194
+
195
+ The `json` output has this shape:
196
+
197
+ ```json
198
+ {
199
+ "status": "failed",
200
+ "passed": false,
201
+ "summary": {
202
+ "errors": 1,
203
+ "warnings": 0,
204
+ "breakingChanges": 0,
205
+ "fixed": 0
206
+ },
207
+ "config": {
208
+ "extends": ["zuplo:recommended"],
209
+ "failOn": "error",
210
+ "source": "default"
211
+ },
212
+ "violations": [
213
+ {
214
+ "ruleId": "zuplo/no-undefined-policies",
215
+ "severity": "error",
216
+ "message": "Policy \"rate-limit-inbound\" is referenced in route but not defined in policies.json",
217
+ "file": "config/routes.oas.json",
218
+ "path": "paths./todos.get.x-zuplo-route.policies.inbound[1]",
219
+ "line": 15,
220
+ "column": 56
221
+ }
222
+ ],
223
+ "breakingChanges": [],
224
+ "fixes": []
225
+ }
226
+ ```
227
+
228
+ See [Lint in CI/CD](./api-lint-ci-cd.mdx) for workflows that use each format.
229
+
230
+ ## Configure the linter
231
+
232
+ With no configuration, the linter runs the `zuplo:recommended` preset and fails
233
+ on errors. To choose a stricter preset, change or scope rules, add OpenAPI and
234
+ OWASP rulesets, write your own rules, or enable breaking change detection, add a
235
+ `zuplo.lint.jsonc` file to the root of your project:
236
+
237
+ ```jsonc title="zuplo.lint.jsonc"
238
+ {
239
+ "$schema": "https://cdn.zuplo.com/schemas/zuplo-lint.json",
240
+ "version": 2,
241
+ "extends": ["zuplo:strict"],
242
+ "rules": {
243
+ "operation-tags": "off",
244
+ },
245
+ "overrides": [
246
+ { "paths": ["/health"], "rules": { "zuplo/require-policy": "off" } },
247
+ ],
248
+ }
249
+ ```
250
+
251
+ See [Lint configuration](./api-lint-configuration.mdx) for every option, and run
252
+ `npx zuplo lint --list-rules` to see every rule you can use.
253
+
254
+ ## Next steps
255
+
256
+ - [Lint configuration](./api-lint-configuration.mdx) — presets, rule severities,
257
+ required policies, and breaking change detection.
258
+ - [Lint rules reference](./api-lint-rules.mdx) — what each rule checks and how
259
+ to fix it.
260
+ - [Custom lint rules](./api-lint-custom-rules.mdx) — write your own rules and
261
+ share them across projects.
262
+ - [Breaking change detection](./api-lint-breaking-changes.mdx) — compare your
263
+ API with a Git branch and catch changes that break consumers.
264
+ - [Lint in CI/CD](./api-lint-ci-cd.mdx) — fail pull requests on lint errors and
265
+ show results in GitHub code scanning.
266
+ - [`zuplo lint` CLI reference](../cli/lint.mdx) — every flag the command
267
+ accepts.