Skip to content
Open
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
17 changes: 16 additions & 1 deletion docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ JSON Schema $Ref Parser comes with built-in support for HTTP and HTTPS, as well
| Option(s) | Type | Description |
| :---------------------------- | :------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `external` | `boolean` | Determines whether external $ref pointers will be resolved. If this option is disabled, then external $ref pointers will simply be ignored. |
| `excludedPathMatcher` | `(string, unknown?) => boolean` | A function that can exclude a path and its descendants from external reference discovery. The callback receives the root-relative JSON Pointer and the value at that path. This is useful when a document contains literal `$ref` properties that should not be downloaded. |
| `file`<br>`http` | `object` `boolean` | These are the built-in resolvers. In addition, you can add [your own custom resolvers](plugins/resolvers.md)<br><br>To disable a resolver, just set it to `false`. |
| `file.order` `http.order` | `number` | Resolvers run in a specific order, relative to other resolvers. For example, a resolver with `order: 5` will run _before_ a resolver with `order: 10`. If a resolver is unable to successfully resolve a path, then the next resolver is tried, until one succeeds or they all fail.<br><br>You can change the order in which resolvers run, which is useful if you know that most of your file references will be a certain type, or if you add [your own custom resolver](plugins/resolvers.md) that you want to run _first_. |
| `file.canRead` `http.canRead` | `boolean`, `RegExp`, `string`, `array`, `function` | Determines which resolvers will be used for which files.<br><br>A regular expression can be used to match files by their full path. A string (or array of strings) can be used to match files by their file extension. Or a function can be used to perform more complex matching logic. See the [custom resolver](plugins/resolvers.md) docs for details. |
Expand All @@ -73,14 +74,28 @@ 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 |

### Excluded path matcher

The `resolve`, `bundle`, and `dereference` options support an `excludedPathMatcher` callback. Each callback receives the same root-relative JSON Pointer format. The schema root is `#`, and child paths look like `#/properties/example`. Paths do not include the source filename or URL, including while crawling content loaded from an external reference.

The optional second argument is the value at the current path. It can be used to distinguish a structural reference from a literal object that happens to contain a `$ref` property:

```javascript
const excludedPathMatcher = (path, value) => {
return path.includes("/example/") && typeof value?.$ref === "string" && !value.$ref.startsWith("#");
};
```

Returning `true` stops that path and its descendants from being processed by that stage. Since resolution runs before bundling or dereferencing, references within a value excluded by `resolve.excludedPathMatcher` remain unresolved during the following stage for the rest of that operation. Internal references may still access properties that physically exist within the excluded value, but `$ref` properties inside it are not followed.

## `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 for each path, which can return true to stop this path and all subpaths from being dereferenced further. The callback receives the root-relative JSON Pointer and the value at that path. 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. |
32 changes: 29 additions & 3 deletions lib/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import $Ref from "./ref.js";
import Pointer from "./pointer.js";
import * as url from "./util/url.js";
import { getSchemaBasePath, getSchemaId, getSchemaIdMode } from "./util/schema-resources.js";
import { wasExcludedDuringResolution } from "./util/resolution-exclusions.js";
import type $Refs from "./refs.js";
import type $RefParser from "./index.js";
import type { ParserOptions } from "./index.js";
Expand Down Expand Up @@ -110,7 +111,14 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
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) &&
!wasExcludedDuringResolution($refs, obj) &&
!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 +163,11 @@ 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 value = obj[key];
if (wasExcludedDuringResolution($refs, value) || isExcludedPath(keyPathFromRoot, value)) {
continue;
}
const childLegacyIdScope = getSchemaIdMode(value, legacyIdScope);
const childScopeBase =
dynamicIdScope && value && typeof value === "object" && !ArrayBuffer.isView(value)
Expand Down Expand Up @@ -248,10 +260,24 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
const shouldResolveOnCwd = $Ref.isExternal$Ref($ref) && options.dereference?.externalReferenceResolution === "root";
const resolutionBase = shouldResolveOnCwd ? url.cwd() : dynamicIdScope ? scopeBase : path;
const $refPath = url.resolve(resolutionBase, $ref.$ref);
const pointer = $refs._resolve($refPath, pathFromRoot, options);
if (pointer === null) {

// Walk values skipped during resolution as literal data. This lets internal pointers reach
// properties that physically exist without resolving nested references in the skipped subtree.
let pointer = $refs._resolve($refPath, pathFromRoot, options, undefined, {
shouldSkipReferenceResolution: (value) => wasExcludedDuringResolution($refs, value),
resolveFinalReference: false,
});
if (pointer === null || pointer.referenceResolutionBlocked) {
return;
}

if (!pointer.crossedResolutionExclusion) {
pointer = $refs._resolve($refPath, pathFromRoot, options);
if (pointer === null) {
return;
}
}

const parsed = Pointer.parse(pathFromRoot);
const depth = parsed.length;
const file = url.stripHash(pointer.path);
Expand Down
44 changes: 37 additions & 7 deletions lib/dereference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import $Ref from "./ref.js";
import Pointer from "./pointer.js";
import * as url from "./util/url.js";
import { getSchemaBasePath, getSchemaIdMode } from "./util/schema-resources.js";
import { wasExcludedDuringResolution } from "./util/resolution-exclusions.js";
import type $Refs from "./refs.js";
import type { DereferenceOptions, ParserOptions } from "./options.js";
import { type $RefParser, type JSONSchema } from "./index.js";
Expand Down Expand Up @@ -94,7 +95,13 @@ 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) &&
!wasExcludedDuringResolution($refs, obj) &&
!isExcludedPath(pathFromRoot, obj)
) {
parents.add(obj);
processedObjects.add(obj);
const currentScopeBase = scopeBase;
Expand Down Expand Up @@ -123,11 +130,10 @@ 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 value = obj[key];
if (wasExcludedDuringResolution($refs, value) || isExcludedPath(keyPathFromRoot, value)) {
continue;
}

const value = obj[key];
const childLegacyIdScope = getSchemaIdMode(value, legacyIdScope);
const childScopeBase =
dynamicIdScope && value && typeof value === "object" && !ArrayBuffer.isView(value)
Expand Down Expand Up @@ -300,7 +306,12 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
}
}

const pointer = $refs._resolve($refPath, path, options);
// Walk values skipped during resolution as literal data. This lets internal pointers reach
// properties that physically exist without resolving nested references in the skipped subtree.
let pointer = $refs._resolve($refPath, path, options, undefined, {
shouldSkipReferenceResolution: (value) => wasExcludedDuringResolution($refs, value),
resolveFinalReference: false,
});

if (pointer === null) {
return {
Expand All @@ -309,6 +320,24 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
};
}

if (pointer.referenceResolutionBlocked) {
return {
circular: false,
value: $ref,
};
}

const crossedResolutionExclusion = pointer.crossedResolutionExclusion;
if (!crossedResolutionExclusion) {
pointer = $refs._resolve($refPath, path, options);
if (pointer === null) {
return {
circular: false,
value: null,
};
}
}

// Check for circular references
const directCircular = pointer.circular;
let circular = directCircular || pointer.chainCircular || parents.has(pointer.value);
Expand All @@ -319,8 +348,9 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
// Dereference the JSON reference
let dereferencedValue = $Ref.dereference($ref, pointer.value, options);

// Crawl the dereferenced value (unless it's circular)
if (!circular) {
// Crawl the dereferenced value unless it is circular or was reached through a resolution
// exclusion. Values reached through an exclusion remain literal so nested $refs are not processed.
if (!circular && !crossedResolutionExclusion) {
// Pointer resolution has already applied every $id scope along the resolved path. Re-applying
// the resolved value's $id here would duplicate relative folder-changing identifiers.
const dereferencedScopeBase = pointer.$ref.dynamicIdScope ? pointer.scopeBase : pointer.$ref.path!;
Expand Down
Loading