The API Commons governance rules must apply to Swagger / OpenAPI 2.0 with parity to OpenAPI 3.x. Spectral auto-detects a document's format (swagger: "2.0" → oas2; openapi: 3.x → oas3/oas3_0/oas3_1) and only runs a rule on a document whose format is in the rule's formats (or on every document when a rule declares no formats). This doc is the authoritative reference for porting.
The port is complete. In the runtime catalog (src/all-rules.json, mirrored by rules/all-rules.yaml) the openapi group is fully format-scoped: 46 oas2 twins, 134 oas3-only rules, 282 format-agnostic rules. That means 328 rules fire on a Swagger 2.0 document (twins + agnostic) and 416 on OpenAPI 3.x. tools/oas2-parity.mjs reports an empty twin worklist and zero untagged 3.x-structure rules (no misfire risk on 2.0). The rules still tagged oas3-only with no 2.0 twin are genuine 3.x-only concepts (webhooks, servers.variables, openIdConnect, the http/bearer security-scheme shape) — correct to leave un-twinned.
Swagger 2.0 is also an explicit grouping in the app, not just a formats tag Spectral resolves at lint time. src/artifacts.ts gives the two OpenAPI artifacts an oasVersion (oas2 / oas3); src/main.ts uses it to (a) select only the matching major's rules when building the active ruleset for validation, and (b) render OpenAPI 3.x and Swagger 2.0 as separate, separately-toggleable groups in the Rules tab. Built-in spectral:oas rules are classified oas2/oas3 in src/spectral.ts (builtinFormatsByName) by calling each rule's format matcher against a minimal 2.0/3.x document. Pasting or uploading a swagger: "2.0" document auto-selects the Swagger 2.0 grouping (detectArtifactType).
Tooling note:
oas2-parity.mjspreviously mis-tagged broadened multi-path rules (agivencarrying both a 3.x and a 2.0 path, e.g.[components.parameters.*, parameters.*]) asoas2-only, which would have stopped them firing on 3.x. Fixed — it now only tags a ruleoas2when itsgivenreferences no 3.x path. It is idempotent and a no-op against the current (complete) ruleset.
- Broaden the
givento a multi-path array when the samethencheck is valid for both formats. One rule, noformats, fires on whichever path exists. Preferred — keeps rule count and names clean.given: [$.components.schemas.*, $.definitions.*]
- Author a format-tagged twin when the structure OR the check genuinely differs. Two rules with distinct keys and
formats: [oas2]/formats: [oas3]. Suffix keys-oas2/-oas3(the compiler now keeps these tokens in names). - Tag a rule
formats: [oas3]when the concept does not exist in 2.0 (see below), and converselyformats: [oas2]for 2.0-only concepts. Never silently leave a 3.x-onlygivenun-tagged — it just no-ops on 2.0 and looks like a gap.
Only port the compiled rules — the -info positive-confirmation variants are dropped at compile, so leave them or mirror them, they won't ship.
| Concept | OpenAPI 3.x | Swagger 2.0 |
|---|---|---|
| Version key | $.openapi (3.x) |
$.swagger (2.0) |
| Servers / base URL | $.servers[*].url |
$.host + $.basePath + $.schemes[*] |
| Reusable schemas | $.components.schemas.* |
$.definitions.* |
| Security schemes | $.components.securitySchemes.* |
$.securityDefinitions.* |
| Reusable parameters | $.components.parameters.* |
$.parameters.* (top-level) |
| Reusable responses | $.components.responses.* |
$.responses.* (top-level) |
| Reusable request bodies | $.components.requestBodies.* |
(none — body is a parameter) |
| Reusable headers/examples/links/callbacks | $.components.headers/examples/links/callbacks.* |
headers only (inline); links/callbacks/examples: none |
| Request body | $.paths[*][*].requestBody + .content['mt'].schema |
$.paths[*][*].parameters[?(@.in=='body')].schema and [?(@.in=='formData')] |
| Request media types | $.paths[*][*].requestBody.content (keys) |
operation/root consumes[*] |
| Response media types | $.paths[*][*].responses[*].content (keys) |
operation/root produces[*] |
| Response body schema | $.paths[*][*].responses[*].content['mt'].schema |
$.paths[*][*].responses[*].schema |
| Parameter data shape | param .schema (all params) |
param .schema only for in: body; in: query/path/header/formData carry type/items/format inline on the parameter |
| Content negotiation | per-operation content maps |
root/operation consumes + produces |
Format-common (usually a single format-agnostic rule — no formats, same given): $.info.*, $.tags[*], $.externalDocs, $.paths (path keys), $.paths[*] (operations), operationId, summary, description, tags, deprecated, $.security, response existence and description, $.paths[*][*].parameters[*].name/in/required.
- 3.x-only (tag
formats: [oas3]):requestBodyobject,content/media-type maps,components.links,components.callbacks,components.requestBodies, cookie parameters (in: cookie), multipleservers,servers.variables,webhooks(3.1),oneOf/anyOf/notat schema roots the 2.0 subset forbids,nullableviatypearrays (3.1). - 2.0-only (tag
formats: [oas2]):$.host,$.basePath,$.schemes,formDataparameters,in: bodyparameter,collectionFormat, top-levelconsumes/produces,$.definitions,$.securityDefinitions. - Shape differs, needs twins: security scheme objects (3.x
type: http|apiKey|oauth2|openIdConnect+scheme/flows; 2.0type: basic|apiKey|oauth2+flow/authorizationUrl), parameter typing (schema vs inline), server/host URL checks (e.g. "https only" →servers[*].urlpattern vsschemescontainshttpsand nothttp).
A — multi-path (schema must have a description):
schema-description: # was components-schemas-description (oas3-only)
given: [$.components.schemas.*, $.definitions.*]
then: { field: description, function: truthy }B — twin (https-only server):
server-https-only-oas3:
formats: [oas3]
given: $.servers[*]
then: { field: url, function: pattern, functionOptions: { match: "^https://" } }
server-https-only-oas2:
formats: [oas2]
given: $
then: { field: schemes, function: schema, functionOptions: { schema: { type: array, not: { contains: { const: http } }, contains: { const: https } } } }C — 3.x-only concept (request body content typed):
request-body-content-schema:
formats: [oas3]
given: $.paths[*][*].requestBody.content.*
then: { field: schema, function: truthy }
# 2.0 analog — body parameter has a schema:
request-body-parameter-schema-oas2:
formats: [oas2]
given: $.paths[*][*].parameters[?(@.in=='body')]
then: { field: schema, function: truthy }- Recompile:
node tools/compile-rules.mjs— it must report a non-zero rule count and NOT prune your new rules; check the console for structural/nimma prunes. - Confirm oas2 coverage exists:
grep -c 'oas2' rules/all-rules.yamlmust be > 0. - Lint a real Swagger 2.0 doc and a 3.x doc through the compiled ruleset; confirm the ported rules fire on 2.0 where expected and don't misfire (e.g., a 3.x-only rule must not error on 2.0 — tag it). Use
samples/openapi-2.0.yaml(added) andsamples/openapi.yaml. - A rule that no-ops on both samples is either mis-pathed or needs a
formatstag — fix it, don't ship it.