---
title: Core API Reference
description: Complete API reference for @specdiff/core
url: https://pr-1-8289d63b6330.thally.app/specdiff/core-api
---

# Core API Reference

Complete API reference for @specdiff/core

The `@specdiff/core` package exports all diffing logic, rule metadata, and utility functions.

```bash
npm install @specdiff/core
```

All exports are ESM. No external dependencies.

## Types

### Severity and Direction

```ts
type Severity = "breaking" | "warning" | "info"
type Direction = "request" | "response" | "neutral"
```

**Severity** indicates the impact of a change:
- **breaking**: the change is incompatible with existing consumers
- **warning**: the change may be problematic depending on usage
- **info**: the change is informational or backwards-compatible

**Direction** is used by JSON Schema rules to adjust severity based on context:
- **request**: the schema describes data sent to the API
- **response**: the schema describes data returned by the API
- **neutral**: direction is not specified (default for `diffJsonSchema`)

### DocumentKind

```ts
type DocumentKind = "openapi" | "json-schema"
```

Identifies whether a document is an OpenAPI 3.x specification or a JSON Schema document.

### RuleCode

```ts
type RuleCode = keyof typeof RULES
```

A union of all valid rule code strings. Use `isRuleCode(value)` to type-guard a string.

Examples: `"type-changed"`, `"property-removed"`, `"endpoint-added"`, `"required-added"`.

### FailThreshold

```ts
type FailThreshold = Severity | "none"
```

Used with `exceedsThreshold` to determine whether a result should trigger failure. `"none"` never triggers failure.

### SchemaChange

A single detected change:

| Field | Type | Description |
|---|---|---|
| `code` | `RuleCode` | The rule that detected this change |
| `severity` | `Severity` | Effective severity after direction adjustments and user overrides |
| `path` | `string` | RFC 6901 JSON pointer (prefixed with `#`) |
| `message` | `string` | Human-readable description of the change |
| `before?` | `unknown` | The value before the change (if captured by the rule) |
| `after?` | `unknown` | The value after the change (if captured by the rule) |

### DiffSummary

Change counts by severity:

| Field | Type | Description |
|---|---|---|
| `breaking` | `number` | Count of breaking changes |
| `warning` | `number` | Count of warnings |
| `info` | `number` | Count of informational changes |
| `total` | `number` | Sum of the above |

### DiffResult

The result of a comparison:

| Field | Type | Description |
|---|---|---|
| `changes` | `SchemaChange[]` | All detected changes, sorted by severity, path, code, message |
| `summary` | `DiffSummary` | Per-severity counts |
| `maxSeverity` | `Severity \| null` | The highest severity present (`"breaking"` > `"warning"` > `"info"`), or `null` if no changes |
| `kind` | `DocumentKind` | The document kind that was compared |

### DiffOptions

Control what comparisons happen and how changes are reported:

| Field | Type | Description |
|---|---|---|
| `ignoreRules?` | `RuleCode[]` | Drop changes from these rule codes |
| `overrides?` | `Partial<Record<RuleCode, Severity>>` | Override severity for specific rules |
| `ignorePaths?` | `string[]` | Drop changes at or beneath these JSON pointer prefixes (leading `#` optional) |
| `direction?` | `Direction` | For JSON Schema only; default `"neutral"`. Adjusts rule severity. |

Example:

```ts
const options: DiffOptions = {
  direction: "request",
  ignoreRules: ["description-changed"],
  overrides: { "default-changed": "breaking" },
  ignorePaths: ["#/paths/~1internal"],
};
```

### RuleInfo

Metadata about a rule:

| Field | Type | Description |
|---|---|---|
| `code` | `RuleCode` | The rule code |
| `defaultSeverity` | `Severity` | Severity before direction/user adjustments |
| `title` | `string` | Short rule title |
| `description` | `string` | What the rule detects |
| `remediation` | `string` | How to fix or suppress the warning |
| `appliesTo` | `DocumentKind \| "both"` | Which document kinds this rule applies to |

### Resolved

Returned by `resolveNode`; represents the result of following a `$ref` chain:

| Field | Type | Description |
|---|---|---|
| `schema` | `unknown` | The dereferenced value |
| `ref` | `string \| undefined` | The final `$ref` key found (if any) |
| `unresolved` | `string \| undefined` | An unresolved `$ref`, if one was encountered and stopped the chain |

### FormatTextOptions

Options for text formatting:

| Field | Type | Description |
|---|---|---|
| `color?` | `boolean` | Enable ANSI color codes (default `false`) |

## Constants

### SEVERITY_ORDER

```ts
const SEVERITY_ORDER = { breaking: 0, warning: 1, info: 2 } as const
```

Numeric order for severity comparison (lower is more severe).

### SEVERITIES

```ts
const SEVERITIES: readonly Severity[] = ["breaking", "warning", "info"]
```

Array of all valid severity values.

## Diff functions

### diffJsonSchema

```ts
function diffJsonSchema(
  before: unknown,
  after: unknown,
  options?: DiffOptions
): DiffResult
```

Compares two JSON Schema documents. The `direction` option (request/response/neutral) adjusts severity for direction-aware rules.

```ts
import { diffJsonSchema, formatText } from "@specdiff/core";

const before = { type: "object", properties: { name: { type: "string" } } };
const after = { type: "object", properties: { id: { type: "number" } } };

const result = diffJsonSchema(before, after, { direction: "response" });
console.log(formatText(result));
```

### diffOpenApi

```ts
function diffOpenApi(
  before: unknown,
  after: unknown,
  options?: DiffOptions
): DiffResult
```

Compares two OpenAPI 3.x documents. Direction is derived from operation context (requests vs responses); the `direction` option is ignored.

```ts
import { diffOpenApi, exceedsThreshold } from "@specdiff/core";

const result = diffOpenApi(beforeSpec, afterSpec, {
  ignoreRules: ["description-changed", "server-removed"],
});

if (exceedsThreshold(result, "breaking")) {
  process.exit(1);
}
```

### diffDocuments

```ts
function diffDocuments(
  before: unknown,
  after: unknown,
  options?: DiffOptions
): DiffResult
```

Auto-detects document kind and calls either `diffOpenApi` or `diffJsonSchema`. Checks the `after` document first, then `before`; either having a string `openapi` key triggers OpenAPI mode.

```ts
const result = diffDocuments(before, after);
console.log(result.kind); // "openapi" or "json-schema"
```

### detectDocumentKind

```ts
function detectDocumentKind(document: unknown): DocumentKind
```

Returns `"openapi"` if the document has a string `openapi` key, else `"json-schema"`.

```ts
const kind = detectDocumentKind({ openapi: "3.0.0", paths: {} });
// kind === "openapi"
```

### exceedsThreshold

```ts
function exceedsThreshold(result: DiffResult, threshold: FailThreshold): boolean
```

Returns `true` when `result.maxSeverity` is at or above `threshold`. Always returns `false` if threshold is `"none"`.

```ts
const result = diffOpenApi(before, after);
if (exceedsThreshold(result, "warning")) {
  console.log("Contains warning or breaking changes");
}
```

## Formatters

### formatText

```ts
function formatText(result: DiffResult, options?: FormatTextOptions): string
```

Human-readable text report grouped by severity. By default no color; pass `{ color: true }` for ANSI codes.

```ts
const result = diffOpenApi(before, after);
console.log(formatText(result, { color: true }));
```

Output example:

```
# Breaking (1 change)
  /paths/~1users/{id} POST: operation-removed

# Warning (2 changes)
  /info: description-changed
  /info: title-changed

# Info (0 changes)
```

### formatMarkdown

```ts
function formatMarkdown(result: DiffResult): string
```

GitHub-Flavored Markdown report with a `## Specdiff report (OpenAPI)` or `## Specdiff report (JSON Schema)` heading, summary table, and per-severity tables.

```ts
const result = diffOpenApi(before, after);
console.log(formatMarkdown(result));
```

Output example:

```markdown
## Specdiff report (OpenAPI)

| Severity | Count |
|----------|-------|
| Breaking | 1     |
| Warning  | 2     |
| Info     | 0     |

### Breaking (1 change)

| Path | Code | Message |
|------|------|---------|
| /paths/~1users/{id} | operation-removed | POST method removed |

### Warning (2 changes)

...
```

### formatJson

```ts
function formatJson(result: DiffResult): string
```

Serializes the `DiffResult` object as pretty-printed JSON (2-space indent). Includes a trailing newline.

```ts
console.log(formatJson(result));
```

### summaryLine

```ts
function summaryLine(result: DiffResult): string
```

Single-line summary suitable for CI output.

```ts
const result = diffOpenApi(before, after);
console.log(summaryLine(result));
// Output: "29 changes: 13 breaking, 7 warning, 9 info"
// Or: "No changes detected."
```

### formatRulesMarkdown

```ts
function formatRulesMarkdown(): string
```

Returns the complete rule catalogue as a Markdown table with columns: code, default severity, title, applies to.

```ts
console.log(formatRulesMarkdown());
```

## Rule functions

### listRules

```ts
function listRules(): RuleInfo[]
```

Returns metadata for all 45 rules.

```ts
const rules = listRules();
rules.forEach((r) => {
  console.log(`${r.code}: ${r.title}`);
});
```

### explainRule

```ts
function explainRule(code: string): RuleInfo | undefined
```

Looks up a single rule by code. Returns `undefined` for unknown codes.

```ts
const info = explainRule("required-added");
if (info) {
  console.log(info.description);
  console.log(info.remediation);
}
```

### isRuleCode

```ts
function isRuleCode(value: string): value is RuleCode
```

Type guard to safely cast a string to `RuleCode`.

```ts
const code = "required-added";
if (isRuleCode(code)) {
  // code is now RuleCode
  const options: DiffOptions = { ignoreRules: [code] };
}
```

### severityFor

```ts
function severityFor(code: RuleCode, direction: Direction): Severity
```

Returns the effective severity of a rule in a given direction, before user overrides are applied.

```ts
const sev = severityFor("required-added", "request");
// "breaking"
const sev2 = severityFor("required-added", "response");
// "info"
```

### RULES

The full rule catalogue object, keyed by rule code. Each entry has `code`, `defaultSeverity`, `title`, `description`, `remediation`, and `appliesTo`.

```ts
const info = RULES["type-changed"];
// { code: "type-changed", defaultSeverity: "breaking", title: "Type changed", ... }
```

### DIRECTION_SEVERITY

A partial map from `RuleCode` to `Record<Direction, Severity>`. Only the 12 rules whose severity varies by direction appear in this table. All other rules use their `defaultSeverity` in every direction.

```ts
const dirSev = DIRECTION_SEVERITY["required-added"];
// { request: "breaking", response: "info", neutral: "breaking" }
```

## Advanced diff utilities

### finalize

```ts
function finalize(
  rawChanges: readonly SchemaChange[],
  kind: DocumentKind,
  options?: DiffOptions
): DiffResult
```

Applies `ignoreRules`, `ignorePaths`, and severity `overrides` to a raw list of changes, then sorts and wraps them into a `DiffResult`. Used internally by `diffJsonSchema`, `diffOpenApi`, and `diffDocuments`.

### summarize

```ts
function summarize(changes: readonly SchemaChange[]): DiffSummary
```

Computes per-severity counts from an array of changes.

### compareChanges

```ts
function compareChanges(a: SchemaChange, b: SchemaChange): number
```

Sort comparator that orders changes by severity (breaking first), then path, code, and message using code-point comparison for determinism across locales.

## JSON pointer helpers

All pointer functions use RFC 6901 notation (`#/segments/with~1slashes`).

### escapePointerSegment

```ts
function escapePointerSegment(segment: string | number): string
```

Escapes a segment for use in a JSON pointer: `~` becomes `~0`, `/` becomes `~1`.

```ts
escapePointerSegment("~foo/bar"); // "~0foo~1bar"
```

### unescapePointerSegment

```ts
function unescapePointerSegment(segment: string): string
```

Inverse of `escapePointerSegment`.

```ts
unescapePointerSegment("~0foo~1bar"); // "~foo/bar"
```

### joinPointer

```ts
function joinPointer(base: string, ...segments: Array<string | number>): string
```

Appends escaped segments to a base pointer.

```ts
joinPointer("#/paths", "~1users/{id}", "post");
// "#/paths/~1users~1{id}/post"
```

### parsePointer

```ts
function parsePointer(pointer: string): string[]
```

Splits a pointer into unescaped segments. Throws on invalid pointer format.

```ts
parsePointer("#/paths/~1users~1{id}/post");
// ["/users/{id}", "post"]
```

### normalizePointer

```ts
function normalizePointer(pointer: string): string
```

Normalizes a pointer to `#/segment/format`. Accepts `paths`, `/paths`, or `#/paths`.

```ts
normalizePointer("paths/x");      // "#/paths/x"
normalizePointer("/paths/x");     // "#/paths/x"
normalizePointer("#/paths/x");    // "#/paths/x"
```

### pointerHasPrefix

```ts
function pointerHasPrefix(pointer: string, prefix: string): boolean
```

Segment-aware prefix test. Returns `true` if `pointer` starts with `prefix` at segment boundaries.

```ts
pointerHasPrefix("#/paths/~1users/post", "#/paths");      // true
pointerHasPrefix("#/paths/~1users/post", "#/paths/~1users"); // true
pointerHasPrefix("#/paths/~1users/post", "#/paths/~1x");     // false
```

### resolvePointer

```ts
function resolvePointer(document: unknown, pointer: string): unknown
```

Walks a document following a JSON pointer. Returns `undefined` when a segment is missing.

```ts
const doc = {
  paths: {
    "/users": { get: { description: "List users" } },
  },
};
resolvePointer(doc, "#/paths/~1users/get/description");
// "List users"

resolvePointer(doc, "#/paths/~1missing");
// undefined
```

## $ref helpers

### isLocalRef

```ts
function isLocalRef(ref: string): boolean
```

Returns `true` if `ref` starts with `#/` or is exactly `#`.

```ts
isLocalRef("#/definitions/User");    // true
isLocalRef("#");                      // true
isLocalRef("https://json-schema.org/schema.json#"); // false
```

### resolveNode

```ts
function resolveNode(
  document: unknown,
  node: unknown,
  maxDepth?: number
): Resolved
```

Follows a chain of local `$ref` keys starting from `node`. Returns `{ schema, ref, unresolved }`. Default `maxDepth` is 32. When exceeded, returns an unresolved result (`schema` is `undefined`, `unresolved` contains the last `$ref`).

```ts
const doc = {
  definitions: {
    User: { $ref: "#/definitions/Person" },
    Person: { type: "object", properties: { name: { type: "string" } } },
  },
};

const node = doc.definitions.User;
const resolved = resolveNode(doc, node);
// resolved.schema === { type: "object", ... }
// resolved.ref === "#/definitions/Person"

// With unresolved ref:
const badNode = { $ref: "#/missing" };
const bad = resolveNode(doc, badNode);
// bad.unresolved === "#/missing"
```

## Usage examples

### Compare with overrides and formatted output

```ts
import { diffJsonSchema, formatMarkdown } from "@specdiff/core";

const before = { properties: { id: { type: "string" } } };
const after = { properties: { id: { type: "integer" } } };

const result = diffJsonSchema(before, after, {
  direction: "response",
  overrides: { "type-changed": "info" },
});

console.log(formatMarkdown(result));
```

### Check threshold and exit

```ts
import { diffOpenApi, exceedsThreshold } from "@specdiff/core";

const result = diffOpenApi(oldSpec, newSpec);
if (exceedsThreshold(result, "breaking")) {
  console.error("Breaking changes detected!");
  process.exit(1);
}
```

### List and filter rules

```ts
import { listRules, isRuleCode } from "@specdiff/core";

const rules = listRules()
  .filter((r) => r.appliesTo === "openapi")
  .sort((a, b) => a.code.localeCompare(b.code));

rules.forEach((r) => console.log(`${r.code}: ${r.title}`));
```

### Navigate and inspect changes

```ts
import { diffOpenApi, resolvePointer } from "@specdiff/core";

const result = diffOpenApi(before, after);

result.changes.forEach((change) => {
  console.log(`${change.code} at ${change.path}`);
  if (change.before !== undefined) {
    console.log(`  Before: ${JSON.stringify(change.before)}`);
  }
  if (change.after !== undefined) {
    console.log(`  After: ${JSON.stringify(change.after)}`);
  }

  // Resolve the change location in the document
  const location = resolvePointer(after, change.path);
  console.log(`  Current value: ${JSON.stringify(location)}`);
});
```