Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/CI-CD.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ jobs:
- name: Run linter
run: pnpm lint

- name: Run type checker
run: pnpm typecheck

- name: Run Node tests
run: pnpm test:node

Expand Down
10 changes: 9 additions & 1 deletion docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ $RefParser.dereference("my-schema.yaml", {
withCredentials: true, // Include auth credentials when resolving HTTP references
},
},
resolveExcludedPathMatcher: (
path,
value, // Don't fetch literal $ref values under any 'example' key
) => path.includes("/example/") && value !== null && typeof value === "object" && "$ref" in value,
dereference: {
circular: false, // Don't allow circular $refs
excludedPathMatcher: (
Expand Down Expand Up @@ -73,14 +77,18 @@ JSON Schema $Ref Parser comes with built-in support for HTTP and HTTPS, as well
| `http.redirects` | `number` | The maximum number of HTTP redirects to follow per file. The default is 5. To disable automatic following of redirects, set this to zero. |
| `http.withCredentials` | `boolean` | Set this to `true` if you're downloading files from a CORS-enabled server that requires authentication |

`resolveExcludedPathMatcher(path, value)` is a top-level option that stops resolution and fetching for a root-relative occurrence and all of its descendants. It is separate from the `resolve` map because every key in that map can be a custom resolver plugin. A reference and its resolved effective value can both be evaluated at the same root-relative path; extended-reference values are merged before their descendants are inspected. Internal alias traversal uses `dereference.maxDepth` as its safety limit.

Resolution, bundling, and dereferencing are stage-local. Configure each stage's matcher when the same literal value should remain untouched throughout a compound operation.

## `dereference` Options

The `dereference` options control how JSON Schema $Ref Parser will dereference `$ref` pointers within the JSON schema.

| Option(s) | Type | Description |
| :-------------------- | :--------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `circular` | `boolean` or `"ignore"` | Determines whether [circular `$ref` pointers](README.md#circular-refs) are handled.<br><br>If set to `false`, then a `ReferenceError` will be thrown if the schema contains any circular references.<br><br> If set to `"ignore"`, then circular references will simply be ignored. No error will be thrown, but the [`$Refs.circular`](refs.md#circular) property will still be set to `true`. |
| `excludedPathMatcher` | `(string) => boolean` | A function, called for each path, which can return true to stop this path and all subpaths from being dereferenced further. This is useful in schemas where some subpaths contain literal `$ref` keys that should not be dereferenced. |
| `excludedPathMatcher` | `(string, unknown?) => boolean` | A function, called with each root-relative path and its current value, which can return true to stop this path and all subpaths from being dereferenced further. This is useful in schemas where some subpaths contain literal `$ref` keys that should not be dereferenced. |
| `onCircular` | `(string) => void` | A function, called immediately after detecting a circular `$ref` with the circular `$ref` in question. |
| `onDereference` | `(string, JSONSchemaObjectType, JSONSchemaObjectType, string) => void` | A function, called immediately after dereferencing, with: the resolved JSON Schema value, the `$ref` being dereferenced, the object holding the dereferenced prop, the dereferenced prop name. |
| `preservedProperties` | `string[]` | An array of properties to preserve when dereferencing a `$ref` schema. Useful if you want to enforce non-standard dereferencing behavior like present in the OpenAPI 3.1 specification where `description` and `summary` properties are preserved when alongside a `$ref` pointer. |
19 changes: 18 additions & 1 deletion lib/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,19 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
options: O,
embeddedResourcePaths: Set<string>,
seen: Set<object>,
matcherAlreadyChecked = false,
) {
const obj = key === null ? parent : parent[key as keyof typeof parent];
const bundleOptions = (options.bundle || {}) as BundleOptions;
const isExcludedPath = bundleOptions.excludedPathMatcher || (() => false);

if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot) && !seen.has(obj)) {
if (
obj &&
typeof obj === "object" &&
!ArrayBuffer.isView(obj) &&
(matcherAlreadyChecked || !isExcludedPath(pathFromRoot, obj)) &&
!seen.has(obj)
) {
// Input schemas are normally JSON trees, but callers can pass pre-circular
// JavaScript objects. Tracking identities keeps those cycles intact without
// recursively walking them until the call stack overflows. It also avoids
Expand Down Expand Up @@ -155,7 +162,16 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
for (const key of keys) {
const keyPath = Pointer.join(path, key);
const keyPathFromRoot = Pointer.join(pathFromRoot, key);
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
if (descriptor?.get && isExcludedPath(keyPathFromRoot, undefined)) {
continue;
}

const value = obj[key];
if (isExcludedPath(keyPathFromRoot, value)) {
continue;
}

const childLegacyIdScope = getSchemaIdMode(value, legacyIdScope);
const childScopeBase =
dynamicIdScope && value && typeof value === "object" && !ArrayBuffer.isView(value)
Expand Down Expand Up @@ -201,6 +217,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
options,
childEmbeddedResourcePaths,
seen,
true,
);
}

Expand Down
11 changes: 8 additions & 3 deletions lib/dereference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
const isExcludedPath = derefOptions.excludedPathMatcher || (() => false);

if (derefOptions?.circular === "ignore" || !processedObjects.has(obj)) {
if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot, obj)) {
parents.add(obj);
processedObjects.add(obj);
const currentScopeBase = scopeBase;
Expand Down Expand Up @@ -122,12 +122,17 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse

const keyPath = Pointer.join(path, key);
const keyPathFromRoot = Pointer.join(pathFromRoot, key);

if (isExcludedPath(keyPathFromRoot)) {
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
if (descriptor?.get && isExcludedPath(keyPathFromRoot, undefined)) {
continue;
}

const value = obj[key];

if (isExcludedPath(keyPathFromRoot, value)) {
continue;
}

const childLegacyIdScope = getSchemaIdMode(value, legacyIdScope);
const childScopeBase =
dynamicIdScope && value && typeof value === "object" && !ArrayBuffer.isView(value)
Expand Down
3 changes: 2 additions & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
} from "./util/errors.js";
import maybe from "./util/maybe.js";
import { getSchemaIdMode, registerSchemaResources, usesDynamicIdScope } from "./util/schema-resources.js";
import type { ParserOptions } from "./options.js";
import type { ExcludedPathMatcher, ParserOptions } from "./options.js";
import { getJsonSchemaRefParserDefaultOptions } from "./options.js";
import type {
$RefsCallback,
Expand Down Expand Up @@ -463,6 +463,7 @@ export {
ParserError,
UnmatchedParserError,
ParserOptions,
ExcludedPathMatcher,
$RefsCallback,
isHandledError,
JSONParserErrorGroup,
Expand Down
41 changes: 32 additions & 9 deletions lib/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,25 @@ import httpResolver from "./resolvers/http.js";

import type { HTTPResolverOptions, JSONSchema, JSONSchemaObject, Plugin, ResolverOptions } from "./types/index.js";

export type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
export type DeepPartial<T> = T extends (...args: never[]) => unknown
? T
: T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;

/** Stops an operation from processing a root-relative schema occurrence and its descendants. */
export type ExcludedPathMatcher = (path: string, value?: unknown) => boolean;

export interface BundleOptions {
/**
* A function, called for each path, which can return true to stop this path and all
* subpaths from being processed further. This is useful in schemas where some
* subpaths contain literal $ref keys that should not be changed.
* subpaths contain literal $ref keys that should not be changed. The current value
* is supplied as the second argument.
*/
excludedPathMatcher?(path: string): boolean;
excludedPathMatcher?: ExcludedPathMatcher;

/**
* Callback invoked during bundling.
Expand Down Expand Up @@ -54,9 +60,10 @@ export interface DereferenceOptions {
/**
* A function, called for each path, which can return true to stop this path and all
* subpaths from being dereferenced further. This is useful in schemas where some
* subpaths contain literal $ref keys that should not be dereferenced.
* subpaths contain literal $ref keys that should not be dereferenced. The current
* value is supplied as the second argument.
*/
excludedPathMatcher?(path: string): boolean;
excludedPathMatcher?: ExcludedPathMatcher;

/**
* Callback invoked during circular reference detection.
Expand Down Expand Up @@ -155,12 +162,28 @@ export interface $RefParserOptions<S extends object = JSONSchema> {
* Determines whether external $ref pointers will be resolved. If this option is disabled, then external `$ref` pointers will simply be ignored.
*/
external?: boolean;

file?: Partial<ResolverOptions<S>> | boolean;
http?: HTTPResolverOptions<S> | boolean;
} & {
[key: string]: Partial<ResolverOptions<S>> | HTTPResolverOptions<S> | boolean | undefined;
};

/**
* A function, called for each root-relative path before references at that occurrence are
* resolved. Returning `true` stops that occurrence and all of its descendants from being
* crawled or fetched. The current value lets callers distinguish structural references from
* literal `$ref` data.
*
* This option is top-level because `resolve` is also an open map of custom resolver plugins;
* placing a callback in that map would make arbitrary function-valued plugins type-check even
* though the runtime ignores them.
*
* This matcher affects resolution only. Configure the bundle or dereference matcher as well
* when the same occurrence should remain unchanged during those later stages.
*/
resolveExcludedPathMatcher?: ExcludedPathMatcher;

/**
* By default, JSON Schema $Ref Parser throws the first error it encounters. Setting `continueOnError` to `true`
* causes it to keep processing as much as possible and then throw a single error that contains all errors
Expand Down
3 changes: 3 additions & 0 deletions lib/ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOpt
*/
path: undefined | string;

/** The physical document occurrence represented by a canonical resource alias. */
sourcePath: undefined | string;

/**
* The resolved value of the JSON reference.
* Can be any JSON type, not just objects. Unknown file types are represented as Buffers (byte arrays).
Expand Down
10 changes: 9 additions & 1 deletion lib/refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,14 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
return $ref;
}

_addAlias(path: string, value: S, pathType?: string | unknown, dynamicIdScope = false, legacyIdScope = false) {
_addAlias(
path: string,
value: S,
pathType?: string | unknown,
dynamicIdScope = false,
legacyIdScope = false,
sourcePath?: string,
) {
const withoutHash = url.stripHash(path);

if (!withoutHash || this._$refs[withoutHash] || this._aliases[withoutHash]) {
Expand All @@ -144,6 +151,7 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
$ref.value = value;
$ref.dynamicIdScope = dynamicIdScope;
$ref.legacyIdScope = legacyIdScope;
$ref.sourcePath = sourcePath;

this._aliases[withoutHash] = $ref;
return $ref;
Expand Down
12 changes: 12 additions & 0 deletions lib/resolve-external.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import $Ref from "./ref.js";
import Pointer from "./pointer.js";
import parse from "./parse.js";
import resolveExternalWithMatcher from "./resolve-external/with-matcher.js";
import * as url from "./util/url.js";
import { isHandledError } from "./util/errors.js";
import { getSchemaBasePath, getSchemaIdMode } from "./util/schema-resources.js";
Expand All @@ -23,11 +24,22 @@ function resolveExternal<S extends object = JSONSchema, O extends ParserOptions<
parser: $RefParser<S, O>,
options: O,
) {
const nestedMatcher = (options.resolve as Record<string, unknown> | undefined)?.excludedPathMatcher;
if (typeof nestedMatcher === "function") {
return Promise.reject(
new TypeError("Use the top-level resolveExcludedPathMatcher option; resolve keys are resolver plugins."),
);
}

if (!options.resolve?.external) {
// Nothing to resolve, so exit early
return Promise.resolve();
}

if (options.resolveExcludedPathMatcher) {
return resolveExternalWithMatcher(parser, options, options.resolveExcludedPathMatcher);
}

try {
const rootScopeBase = parser.$refs._root$Ref.dynamicIdScope
? getSchemaBasePath(parser.$refs._root$Ref.path!, parser.schema, parser.$refs._root$Ref.legacyIdScope)
Expand Down
Loading