diff --git a/docs/options.md b/docs/options.md index 4dfc3107..5d78e6dd 100644 --- a/docs/options.md +++ b/docs/options.md @@ -83,3 +83,4 @@ The `dereference` options control how JSON Schema $Ref Parser will dereference ` | `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. | | `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. | diff --git a/lib/dereference.ts b/lib/dereference.ts index 886b2cbe..c0f989de 100644 --- a/lib/dereference.ts +++ b/lib/dereference.ts @@ -123,7 +123,31 @@ function crawl = Parse circular = dereferenced.circular; // Avoid pointless mutations; breaks frozen objects to no profit if (obj[key] !== dereferenced.value) { + // If we have properties we want to preserve from our dereferenced schema then we need + // to copy them over to our new object. + const preserved: Map = new Map(); + if (derefOptions?.preservedProperties) { + if (typeof obj[key] === "object" && !Array.isArray(obj[key])) { + derefOptions?.preservedProperties.forEach((prop) => { + if (prop in obj[key]) { + preserved.set(prop, obj[key][prop]); + } + }); + } + } + obj[key] = dereferenced.value; + + // If we have data to preserve and our dereferenced object is still an object then + // we need copy back our preserved data into our dereferenced schema. + if (derefOptions?.preservedProperties) { + if (preserved.size && typeof obj[key] === "object" && !Array.isArray(obj[key])) { + preserved.forEach((value, prop) => { + obj[key][prop] = value; + }); + } + } + derefOptions?.onDereference?.(value.$ref, obj[key], obj, key); } } else { diff --git a/lib/options.ts b/lib/options.ts index f7b9af8a..a66b1e00 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -46,6 +46,16 @@ export interface DereferenceOptions { */ onDereference?(path: string, value: JSONSchemaObject, parent?: JSONSchemaObject, parentPropName?: string): void; + /** + * 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. + * + * If none supplied then no properties will be preserved and the object will be fully replaced + * with the dereferenced `$ref`. + */ + preservedProperties?: string[]; + /** * Whether a reference should resolve relative to its directory/path, or from the cwd * diff --git a/lib/util/errors.ts b/lib/util/errors.ts index 3a643bd8..af08d230 100644 --- a/lib/util/errors.ts +++ b/lib/util/errors.ts @@ -126,7 +126,7 @@ export class MissingPointerError extends JSONParserError { public targetToken: any; public targetRef: string; public targetFound: string; - public parentPath: string; + public parentPath: string; constructor(token: any, path: any, targetRef: any, targetFound: any, parentPath: any) { super(`Missing $ref pointer "${getHash(path)}". Token "${token}" does not exist.`, stripHash(path)); diff --git a/test/specs/dereference-preservedProperties/dereference-preservedProperties.spec.ts b/test/specs/dereference-preservedProperties/dereference-preservedProperties.spec.ts new file mode 100644 index 00000000..95ec0fda --- /dev/null +++ b/test/specs/dereference-preservedProperties/dereference-preservedProperties.spec.ts @@ -0,0 +1,41 @@ +import { describe, it } from "vitest"; +import $RefParser from "../../../lib/index.js"; +import pathUtils from "../../utils/path.js"; + +import { expect } from "vitest"; +import type { Options } from "../../../lib/options"; + +describe("dereference.preservedProperties", () => { + it("should preserve properties", async () => { + const parser = new $RefParser(); + const schema = pathUtils.rel("test/specs/dereference-preservedProperties/dereference-preservedProperties.yaml"); + const options = { + dereference: { + preservedProperties: ["description"], + }, + } as Options; + const res = await parser.dereference(schema, options); + + expect(res).to.deep.equal({ + title: "Person", + required: ["name"], + type: "object", + definitions: { + name: { + type: "string", + description: "Someone's name", + }, + }, + properties: { + name: { + type: "string", + description: "Someone's name", + }, + secretName: { + type: "string", + description: "Someone's secret name", + }, + }, + }); + }); +}); diff --git a/test/specs/dereference-preservedProperties/dereference-preservedProperties.yaml b/test/specs/dereference-preservedProperties/dereference-preservedProperties.yaml new file mode 100644 index 00000000..7a067f5e --- /dev/null +++ b/test/specs/dereference-preservedProperties/dereference-preservedProperties.yaml @@ -0,0 +1,16 @@ +title: Person +required: + - name +type: object +definitions: + name: + type: string + description: Someone's name +properties: + name: + $ref: "#/definitions/name" + secretName: + $ref: "#/definitions/name" + # Despite "Someone's name" being the description of the referenced `name` schema our overwritten + # description should be preserved instead. + description: Someone's secret name