Skip to content

Commit ae7d95b

Browse files
authored
feat: dereference.preservedProperties for preserving data during dereferencing (#369)
* feat: `dereference.preservedProperties` for preserving data during dereferencing * fix: typo * fix: wrapping logic in a conditional
1 parent 9e2a1e6 commit ae7d95b

File tree

6 files changed

+93
-1
lines changed

6 files changed

+93
-1
lines changed

docs/options.md

+1
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,4 @@ The `dereference` options control how JSON Schema $Ref Parser will dereference `
8383
| `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. |
8484
| `onCircular` | `(string) => void` | A function, called immediately after detecting a circular `$ref` with the circular `$ref` in question. |
8585
| `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. |
86+
| `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. |

lib/dereference.ts

+24
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,31 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
123123
circular = dereferenced.circular;
124124
// Avoid pointless mutations; breaks frozen objects to no profit
125125
if (obj[key] !== dereferenced.value) {
126+
// If we have properties we want to preserve from our dereferenced schema then we need
127+
// to copy them over to our new object.
128+
const preserved: Map<string, unknown> = new Map();
129+
if (derefOptions?.preservedProperties) {
130+
if (typeof obj[key] === "object" && !Array.isArray(obj[key])) {
131+
derefOptions?.preservedProperties.forEach((prop) => {
132+
if (prop in obj[key]) {
133+
preserved.set(prop, obj[key][prop]);
134+
}
135+
});
136+
}
137+
}
138+
126139
obj[key] = dereferenced.value;
140+
141+
// If we have data to preserve and our dereferenced object is still an object then
142+
// we need copy back our preserved data into our dereferenced schema.
143+
if (derefOptions?.preservedProperties) {
144+
if (preserved.size && typeof obj[key] === "object" && !Array.isArray(obj[key])) {
145+
preserved.forEach((value, prop) => {
146+
obj[key][prop] = value;
147+
});
148+
}
149+
}
150+
127151
derefOptions?.onDereference?.(value.$ref, obj[key], obj, key);
128152
}
129153
} else {

lib/options.ts

+10
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ export interface DereferenceOptions {
4646
*/
4747
onDereference?(path: string, value: JSONSchemaObject, parent?: JSONSchemaObject, parentPropName?: string): void;
4848

49+
/**
50+
* An array of properties to preserve when dereferencing a `$ref` schema. Useful if you want to
51+
* enforce non-standard dereferencing behavior like present in the OpenAPI 3.1 specification where
52+
* `description` and `summary` properties are preserved when alongside a `$ref` pointer.
53+
*
54+
* If none supplied then no properties will be preserved and the object will be fully replaced
55+
* with the dereferenced `$ref`.
56+
*/
57+
preservedProperties?: string[];
58+
4959
/**
5060
* Whether a reference should resolve relative to its directory/path, or from the cwd
5161
*

lib/util/errors.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export class MissingPointerError extends JSONParserError {
126126
public targetToken: any;
127127
public targetRef: string;
128128
public targetFound: string;
129-
public parentPath: string;
129+
public parentPath: string;
130130
constructor(token: any, path: any, targetRef: any, targetFound: any, parentPath: any) {
131131
super(`Missing $ref pointer "${getHash(path)}". Token "${token}" does not exist.`, stripHash(path));
132132

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { describe, it } from "vitest";
2+
import $RefParser from "../../../lib/index.js";
3+
import pathUtils from "../../utils/path.js";
4+
5+
import { expect } from "vitest";
6+
import type { Options } from "../../../lib/options";
7+
8+
describe("dereference.preservedProperties", () => {
9+
it("should preserve properties", async () => {
10+
const parser = new $RefParser();
11+
const schema = pathUtils.rel("test/specs/dereference-preservedProperties/dereference-preservedProperties.yaml");
12+
const options = {
13+
dereference: {
14+
preservedProperties: ["description"],
15+
},
16+
} as Options;
17+
const res = await parser.dereference(schema, options);
18+
19+
expect(res).to.deep.equal({
20+
title: "Person",
21+
required: ["name"],
22+
type: "object",
23+
definitions: {
24+
name: {
25+
type: "string",
26+
description: "Someone's name",
27+
},
28+
},
29+
properties: {
30+
name: {
31+
type: "string",
32+
description: "Someone's name",
33+
},
34+
secretName: {
35+
type: "string",
36+
description: "Someone's secret name",
37+
},
38+
},
39+
});
40+
});
41+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
title: Person
2+
required:
3+
- name
4+
type: object
5+
definitions:
6+
name:
7+
type: string
8+
description: Someone's name
9+
properties:
10+
name:
11+
$ref: "#/definitions/name"
12+
secretName:
13+
$ref: "#/definitions/name"
14+
# Despite "Someone's name" being the description of the referenced `name` schema our overwritten
15+
# description should be preserved instead.
16+
description: Someone's secret name

0 commit comments

Comments
 (0)