rules-system.md

July 8, 2026 · View on GitHub

Rules System: Walker, Visitors, and Nodes

This is the most important pattern to understand when working in packages/core.

Concepts

Node — a typed object in the parsed API description AST. Every node has a name that matches its spec concept: Schema, Operation, Server, Parameter, Response, etc. The full list of node types per spec is in packages/core/src/types/.

Visitor — an object whose keys are node names. When the Walker enters or leaves a node of that type, it calls the corresponding visitor hook. Visitor names mirror node names exactly. The full visitor type map is in packages/core/src/visitors.ts.

Walker — the engine in packages/core/src/walk.ts (walkDocument). It recursively traverses the parsed document, resolves $ref references, and invokes registered visitors at each node.

Visitor hooks

Each key in a visitor object can be either a plain function (shorthand for enter) or an object with up to three hooks:

HookWhen it runs
enter(node, ctx)When the Walker first arrives at this node
leave(node, ctx)After all child nodes have been visited; all $refs are resolved by this point
skip(node, ctx)Called before enter; return true to skip this visitor entirely for this node

Context object (ctx)

Every visitor hook receives a context object with:

PropertyTypeDescription
report(problem)functionEmit a lint problem
locationLocationJSON pointer + source of the current node
keystring | numberKey of this node within its parent
parentanyParent node object
resolve(ref)functionResolve a $ref to its target node and location
typeNormalizedNodeTypeType descriptor for the current node
specVersionSpecVersionFor example, 'OAS3_0', 'OAS3_1'
getVisitorData()functionShared data store scoped to the current rule invocation

Rule function signature

A rule is a factory function that receives rule options and returns a visitor object. The type depends on the target spec:

import type { Oas3Rule } from '../../visitors.js';

// Factory receives rule options, returns a visitor
export const MyRule: Oas3Rule = (options) => {
  // State can be kept here — it is scoped to one document walk
  return {
    NodeName(node, ctx) {
      /* shorthand enter */
    },

    OtherNode: {
      enter(node, ctx) {
        /* ... */
      },
      leave(node, ctx) {
        /* ... */
      },
      skip(node, ctx) {
        return false;
      },
    },
  };
};

Available rule types: Oas3Rule, Oas3_1Rule, Oas2Rule, Async2Rule, Async3Rule, ArazzoRule.

Minimal rule example

// packages/core/src/rules/oas3/no-server-trailing-slash.ts
import type { Oas3Rule } from '../../visitors.js';

export const NoServerTrailingSlash: Oas3Rule = () => {
  return {
    Server(server, { report, location }) {
      if (server.url?.endsWith('/') && server.url !== '/') {
        report({
          message: 'Server `url` should not have a trailing slash.',
          location: location.child(['url']),
        });
      }
    },
  };
};

Stateful rule example (using enter + leave)

// packages/core/src/rules/oas3/no-unused-components.ts
import type { Oas3Rule } from '../../visitors.js';

export const NoUnusedComponents: Oas3Rule = () => {
  const components = new Map<string, { used: boolean; location: Location; name: string }>();

  return {
    // Track every $ref resolution — mark the target as used
    ref(ref, { type, resolve, key, location }) {
      const resolved = resolve(ref);
      if (resolved.location) {
        components.set(resolved.location.absolutePointer, {
          used: true,
          name: key.toString(),
          location,
        });
      }
    },

    // Report unused components only after the entire document has been walked
    Root: {
      leave(_, { report }) {
        components.forEach((info) => {
          if (!info.used) {
            report({
              message: `Component: "${info.name}" is never used.`,
              location: info.location.key(),
            });
          }
        });
      },
    },

    NamedSchemas: {
      Schema(schema, { location, key }) {
        components.set(location.absolutePointer, { used: false, location, name: key.toString() });
      },
    },
  };
};

Register a new rule

After creating the rule file, register it in the spec index:

// packages/core/src/rules/oas3/index.ts
import { NoMyRule } from './no-my-rule.js';

export const Oas3Rules = {
  // ...existing rules...
  'no-my-rule': NoMyRule,
};

Configurable rules (Assertions)

Users can define their own rules in redocly.yaml using the built-in Assertion system (packages/core/src/rules/common/assertions/asserts.ts). Instead of writing TypeScript, the user declares a subject node type and a set of assertion checks. Internally, the subject type is converted into a visitor automatically.

rules:
  rule/path-exclude-pattern:
    subject:
      type: Paths # node type → becomes a visitor
    assertions:
      notPattern: \/wrong

Prefer implementing actual rule code over adding assertion-based rules when contributing to the core rule set.