A standalone specification and JSON Schema for the Spectral ruleset format.
Spectral rulesets are the most widely adopted machine-readable format for API governance rules. Thousands of organizations describe their API style guides, security baselines, and design standards as a Spectral ruleset, then run those rules in CI against every OpenAPI, AsyncAPI, Arazzo, and JSON Schema document they ship.
But the format has never been specified. It exists only as configuration for one linter, described by five internal draft-07 meta-schemas coupled to that linter's runtime, versioned with the tool rather than on its own. That makes an organization's governance rules — arguably the most durable, most portable artifact an API program produces — dependent on the lifecycle of a single CLI.
This repository treats the ruleset as a first-class artifact in its own right: an independently versioned specification with a portable JSON Schema, so rules can be authored, validated, published, exchanged, and consumed by any tool, not just by the linter that happens to run them.
- Specification — this document.
- JSON Schema —
schema/v1/spectral-ruleset.schema.json(JSON Schema draft 2020-12). - Reference implementation — api-commons/spectral-cli, the API Commons build of stoplightio/spectral.
Status: draft, and deliberately unnamed. The format described here is the Spectral ruleset format as implemented today, documented faithfully and without extension. The name of the specification is not yet settled. This repository and the reference implementation move together and will be rebranded together; until then both keep the upstream name so the lineage stays obvious.
The rules and the tool that runs them have always shipped as one thing, and that coupling is the failure this repository is meant to fix. When the linter stops moving, the format stops moving with it — and an organization's governance rules are usually the most durable thing an API program produces. They outlast the CLI, the vendor, and often the API itself.
So: the rules stand on their own. Specified, independently versioned, portable, and implementable by anyone. The reference implementation stays aligned with this document rather than the other way around — and other engines are welcome to implement it. That includes vacuum, which is a supported and valued implementation; a written specification with a public conformance suite is precisely what lets several engines coexist honestly instead of drifting apart on undocumented behavior.
| What | Where |
|---|---|
| Problems with the format, the schema, or this spec | spectral-rules/issues |
| Problems with the CLI or engine | spectral-cli/issues |
| Direction, governance, naming, where this should live | the discussion thread |
If you depend on the Spectral ruleset format, say so in the discussion. Knowing who relies on it is what makes the case for a real, permanent home.
- Using the schema
- The Ruleset Object
- Rules
extendsaliasesoverridesparserOptions- Custom functions
- Extension properties
- Conformance notes
- Versioning
- Provenance and license
The schema validates a ruleset expressed as JSON or YAML. Its canonical $id is:
https://raw.githubusercontent.com/api-commons/spectral-rules/main/schema/v1/spectral-ruleset.schema.json
In an editor. VS Code and other JSON Schema-aware editors give you completion and inline
validation. The format reserves no $schema key and rejects unknown root properties, so bind
the schema from outside the document rather than inside it.
For YAML rulesets, a modeline comment:
# yaml-language-server: $schema=https://raw.githubusercontent.com/api-commons/spectral-rules/main/schema/v1/spectral-ruleset.schema.json
extends: spectral:oasFor JSON rulesets, map it by filename in .vscode/settings.json:
{
"json.schemas": [
{
"fileMatch": [".spectral.json", "*.ruleset.json"],
"url": "https://raw.githubusercontent.com/api-commons/spectral-rules/main/schema/v1/spectral-ruleset.schema.json"
}
]
}Do not add a "$schema" property to the ruleset itself — the linter will reject it.
In CI. Any draft 2020-12 validator will do:
npx ajv-cli validate --spec=draft2020 \
-s schema/v1/spectral-ruleset.schema.json \
-d .spectral.yamlValidating a ruleset against this schema is a stricter and earlier check than running the
linter: it catches misspelled keywords, malformed given expressions, and structurally
invalid overrides before any document is linted.
A ruleset is a single JSON or YAML object. It MUST declare at least one of extends,
rules, or overrides — a ruleset that contributes nothing is invalid.
| Field | Type | Description |
|---|---|---|
description |
string |
Human-readable description of what this ruleset governs. |
documentationUrl |
string (URL) |
Documentation for the ruleset as a whole. Used as the fallback documentation link for any rule that does not declare its own. |
formats |
Formats | Document formats the ruleset applies to. Default for every rule that does not declare its own. When omitted, rules apply to every document. |
extends |
Extends | Other rulesets this one inherits from. |
rules |
object |
Map of rule name to Rule Object. |
aliases |
Aliases | Named, reusable path expressions. |
overrides |
Overrides | Per-file and per-location adjustments. |
parserOptions |
ParserOptions | Severities for problems in the document itself, before rules run. |
functions |
string[] |
Names of custom functions to load. Serialized rulesets only. |
functionsDir |
string |
Directory custom functions load from. Defaults to functions. |
x-* |
any | Extension properties. |
No other properties are permitted at the root.
A rule name is the stable identifier reported in lint results and used to reference the rule
from a consumer's extends or overrides. Rule names are the ruleset's public API: renaming
one silently breaks every downstream ruleset that disables or re-severities it.
description: Acme API style guide
documentationUrl: https://engineering.acme.com/api/governance
extends: [[spectral:oas, recommended]]
formats: [oas3]
rules:
acme-operation-summary:
description: Every operation needs a summary.
severity: error
given: $.paths[*][get,put,post,delete,options,head,patch,trace]
then:
field: summary
function: truthyA rule is the unit of governance: where to look in a document, and what must hold there.
| Field | Type | Required | Description |
|---|---|---|---|
given |
Given | ✔ | The part or parts of the document the rule applies to. |
then |
Then | ✔ | The assertion, or assertions, applied to each matched node. |
description |
string |
What the rule requires, in prose. | |
message |
string |
Template for the reported message. See message. |
|
severity |
Severity | How results are reported. Defaults to warn. |
|
formats |
Formats | Formats this rule applies to. Overrides the ruleset-level formats. |
|
documentationUrl |
string (URL) |
Where to read about the rule and how to satisfy it. | |
recommended |
boolean |
Whether the rule is on when a consumer extends the ruleset in recommended mode. Defaults to true. |
|
resolved |
boolean |
Whether the rule sees the document with $refs resolved. Defaults to true. |
|
tags |
string[] |
Free-form labels for grouping and filtering. | |
type |
"style" | "validation" |
Coarse classification. validation asserts correctness, style asserts convention. |
|
extensions |
object |
Arbitrary consumer-defined metadata, ignored by the engine. | |
x-* |
any | Extension properties. |
No other properties are permitted on a rule.
resolved deserves a note. By default a rule runs against the document after $refs have
been resolved, which is what you want when asserting on the effective shape of an API. Set
resolved: false when the rule is about references themselves — their form, their targets,
whether they are used at all — because in the resolved document those references no longer
exist.
given selects the nodes a rule runs against. It is either a single path expression or a
non-empty array of them.
A path expression is a string that is one of:
- a JSONPath expression, beginning with
$— for example$.paths[*]or$..parameters[?(@.in === 'header')].name; - an alias reference, beginning with
#and naming an alias, optionally followed by a JSONPath subset — for example#Info.description.
Anything else is invalid. A bare property name such as paths is a common authoring mistake
and is rejected by the schema.
given:
- $.info
- "#OperationObject"then is one function invocation, or an array of them all applied to every node given
matched.
| Field | Type | Required | Description |
|---|---|---|---|
function |
string |
✔ | Name of a core function or of a custom function. |
field |
string |
Narrow the assertion to one property of the matched node rather than the node itself. The special value @key targets the node's own key. |
|
functionOptions |
object |
Options passed to the function. Its accepted shape is defined by the function. |
The reference implementation ships these core functions:
| Function | Asserts that the target… |
|---|---|
truthy |
is present and not false, "", 0, null, or undefined. |
falsy |
is absent or falsy. |
defined |
is present. |
undefined |
is absent. |
pattern |
matches (match) and/or does not match (notMatch) a regular expression. |
casing |
uses a given casing convention (flat, camel, pascal, kebab, cobol, snake, macro). |
length |
has a min/max length, or numeric value within bounds. |
alphabetical |
is sorted, optionally keyedBy a property. |
enumeration |
is one of a set of values. |
schema |
conforms to a supplied JSON Schema. |
xor |
has exactly one of a set of properties. |
or |
has at least one of a set of properties. |
unreferencedReusableObject |
(as a reusable-object container) has no unreferenced members. |
then:
- field: summary
function: truthy
- field: summary
function: length
functionOptions:
max: 120How a rule's results are reported, and whether the rule runs at all.
| Name | Numeric |
|---|---|
error |
0 |
warn |
1 |
info |
2 |
hint |
3 |
off |
-1 |
Both spellings are accepted wherever a severity is expected. When a rule omits severity, it
defaults to warn.
A format is a predicate that decides whether a given document is of a particular kind. Scoping a rule to a format means the rule only runs against matching documents — so one ruleset can govern an OpenAPI 3.1 description and an AsyncAPI 2.6 description without either set of rules firing on the wrong one.
formats is an array of format identifiers. The reference implementation registers:
| Family | Identifiers |
|---|---|
| OpenAPI | oas2, oas3, oas3_0, oas3_1 |
| AsyncAPI | asyncapi2, aas2, aas2_0–aas2_6, aas3, aas3_0 |
| Arazzo | arazzo1_0 |
| JSON Schema | jsonSchema, jsonSchemaLoose, jsonSchemaDraft4, jsonSchemaDraft6, jsonSchemaDraft7, jsonSchemaDraft2019_09, jsonSchemaDraft2020_12 |
The schema lists these as known values for editor completion but accepts any string, so rulesets targeting formats added later — or registered by a host application — remain valid.
A rule's formats overrides the ruleset's. Omitting both means the rule runs against every
document.
Overrides the message reported for a result. Five placeholders are substituted:
| Placeholder | Resolves to |
|---|---|
{{error}} |
The message the function produced. |
{{description}} |
The rule's description. |
{{path}} |
The full path to the offending value. |
{{property}} |
The key or index of the offending value. |
{{value}} |
The offending value itself. |
message: "{{description}} — `{{property}}` was {{value}}"Inside rules, a value that is not an object adjusts a rule inherited through
extends rather than defining a new one:
- a severity name (
error,warn,info,hint,off) re-severities the rule; falsedisables the rule;trueenables it at its declared severity.
extends: spectral:oas
rules:
operation-tags: error # raise it
info-contact: false # switch it off
oas3-schema: true # switch it back onNote that the numeric severities are not accepted in this position; only the names are.
Inherit from one or more other rulesets. An entry is a built-in identifier
(spectral:oas, spectral:asyncapi, spectral:arazzo), a file path, an npm module
specifier, or a URL.
extends: spectral:oasAn entry may instead be a two-element tuple pairing a ruleset with the set of its rules to enable:
| Mode | Enables |
|---|---|
recommended |
Only rules with recommended: true. The default when a bare string is used. |
all |
Every rule in the ruleset. |
off |
No rules — inherit the definitions without turning any of them on, then enable the ones you want by name. |
extends:
- [spectral:oas, all]
- [./team-baseline.yaml, off]
- https://example.com/rulesets/security.yamloff is the mode that makes a large inherited ruleset adoptable: take the definitions, start
from silence, and switch rules on deliberately.
Complex JSONPath expressions repeated across rules are the main source of drift in a large
ruleset. An alias names one, and rules reference it from given with a leading #.
Alias names must match ^[A-Za-z][A-Za-z0-9_-]*$.
A simple alias is an array of path expressions — note that it is always an array, never a bare string:
aliases:
Info:
- $..info
InfoDescription:
- "#Info.description"
PathItemObject:
- $.paths[*]
OperationObject:
- "#PathItemObject[get,put,post,delete,options,head,patch,trace]"Aliases compose: InfoDescription and OperationObject above are defined in terms of other
aliases.
A scoped alias resolves to different expressions depending on the format of the document being linted, which is how one rule can govern structurally different specs:
aliases:
ParameterObject:
description: Every Parameter Object, wherever this spec version keeps them.
targets:
- formats: [oas2]
given:
- $.parameters[*]
- formats: [oas3]
given:
- $.components.parameters[*]Each target requires both formats and given, and given in a target is always an array.
Aliases may be declared at the root of the ruleset or within an override.
Overrides apply adjustments to a subset of the documents being linted, matched by glob. They are an ordered array — later matching entries win over earlier ones, and every override wins over the base ruleset. The array must not be empty.
There are two forms, distinguished by whether the files globs carry a JSON Pointer fragment.
File-scoped — globs without a # fragment. May change rules, extends, formats,
parserOptions, and aliases, and must contain at least extends or rules:
overrides:
- files: ["legacy/**/*.yaml"]
rules:
acme-operation-summary: warn
- files: ["v3/**/*.yaml"]
formats: [oas3_1]
extends: [[./strict.yaml, all]]Pointer-scoped — every glob carries a # fragment naming a location inside the document.
May change only rule severities, and must contain rules:
overrides:
- files: ["api.yaml#/paths/~1legacy~1users"]
rules:
acme-operation-summary: "off"This asymmetry is deliberate: a waiver aimed at one path in one file can silence a rule there, but it cannot quietly redefine what the rule means.
Severities for problems found while parsing the document, before any rule runs.
| Field | Description | Default |
|---|---|---|
duplicateKeys |
Duplicate keys in the document. | error |
incompatibleValues |
YAML values that have no JSON representation. | error |
parserOptions:
duplicateKeys: error
incompatibleValues: warnWhen the core functions cannot express an assertion, a serialized ruleset can name custom functions to load:
functions:
- dictionary
functionsDir: ./my-functions # optional; defaults to "functions"
rules:
no-evil-words:
message: "{{error}}"
given:
- $.info.title
- $.info.description
then:
function: dictionaryEach name in functions resolves to <functionsDir>/<name>.js, relative to the ruleset file.
Two caveats worth stating plainly. First, custom functions are the point at which a ruleset stops being portable data and starts being code — a ruleset that names them cannot be fully understood, or safely consumed, from the ruleset document alone. Second, in the reference implementation that code is not sandboxed; extending an untrusted remote ruleset that carries functions is arbitrary code execution. Prefer the core functions where they suffice.
Properties beginning with x- are permitted at the root of the ruleset and on any rule, and
are ignored by the engine. Use them to carry ownership, tickets, review dates, or anything
else your own tooling needs:
x-owner: platform-team
rules:
acme-operation-summary:
x-jira: GOV-142
given: $.paths[*][*]
then:
field: summary
function: truthyRules additionally have an extensions object for the same purpose.
This specification documents the format as the reference implementation actually behaves, including where that behavior is looser than a clean design would be. Three points where the schema deliberately matches the implementation rather than tightening it:
thenaccepts unknown properties. A misspelledfunctionOption(singular) is not rejected; it is silently ignored, and the function runs with its defaults. The schema permits this so that valid rulesets are not rejected, but authors should treat any unrecognized key insidethenas a bug.- Format identifiers are open. Any string is accepted, so a typo like
oas31validates and simply matches no document — meaning the rule never fires. Rules that never fire look identical to rules that always pass. The known identifiers are listed in the schema for editor completion; check them. - Rule shorthands are name-only.
severity: 1is valid inside a rule definition, butmy-rule: 1insiderulesis not — the shorthand position takes only the severity names and booleans. The asymmetry is inherited, not intended. - There is no self-describing key. The root permits only the properties above plus
x-*, so a ruleset cannot point at its own schema with a$schemaproperty the way most JSON formats can. Schema binding has to happen outside the document, as shown in Using the schema. A format specified from the start would have reserved that key.
Two further limits on scope:
- The schema describes the serialized (JSON/YAML) form. Rulesets authored in JavaScript can inline function implementations and whole ruleset objects, which have no JSON representation and are therefore out of scope.
givenvalues are checked for shape — that they are JSONPath expressions or alias references — not parsed as JSONPath. A syntactically invalid JSONPath will pass the schema and fail at lint time.
The schema lives under schema/v1/ and is versioned independently of the reference
implementation. Within v1, changes will be additive and backward-compatible: new optional
properties, newly registered format identifiers, clarified descriptions. Anything that would
reject a previously valid ruleset gets a new directory and a new $id.
The format described here originates in
Spectral by Stoplight, licensed Apache-2.0. This
specification and schema were derived from the reference implementation's own internal
validation meta-schemas — ruleset.schema.json, rule.schema.json, shared.json,
json-extensions.json, and js-extensions.json in packages/core/src/ruleset/meta/ — and
from its guides, then re-expressed as a single self-contained JSON Schema draft 2020-12
document with the implementation's runtime hooks and validator-specific error-message
extensions removed.
The schema is verified against the real-world rulesets published across the API Commons, which it accepts, and against a suite of malformed rulesets, which it rejects.
This repository is licensed Apache-2.0.
Maintained by API Commons — open, machine-readable API building blocks.