@hey-api/json-schema-ref-parser 1.3.1 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hey-api/json-schema-ref-parser",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
4
4
  "description": "Parse, Resolve, and Dereference JSON Schema $ref pointers",
5
5
  "keywords": [
6
6
  "$ref",
@@ -45,19 +45,17 @@
45
45
  "dependencies": {
46
46
  "@jsdevtools/ono": "7.1.3",
47
47
  "@types/json-schema": "7.0.15",
48
- "js-yaml": "4.1.1"
48
+ "yaml": "2.8.3"
49
49
  },
50
50
  "devDependencies": {
51
- "@types/js-yaml": "4.0.9",
52
- "typescript": "5.9.3"
51
+ "typescript": "6.0.2"
53
52
  },
54
53
  "engines": {
55
- "node": ">=20.19.0"
54
+ "node": ">=22.13.0"
56
55
  },
57
56
  "scripts": {
58
- "build": "tsdown && pnpm check-exports",
59
- "check-exports": "attw --pack . --profile esm-only --ignore-rules cjs-resolves-to-esm",
57
+ "build": "tsdown",
60
58
  "dev": "tsdown --watch",
61
- "typecheck": "tsc --noEmit"
59
+ "typecheck": "tsgo --noEmit"
62
60
  }
63
61
  }
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
 
@@ -11,6 +12,10 @@ const __dirname = path.dirname(__filename);
11
12
  const getSnapshotsPath = () => path.join(__dirname, '__snapshots__');
12
13
  const getTempSnapshotsPath = () => path.join(__dirname, '.gen', 'snapshots');
13
14
 
15
+ const writeJsonFile = (filePath: string, value: unknown) => {
16
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
17
+ };
18
+
14
19
  /**
15
20
  * Helper function to compare a bundled schema with a snapshot file.
16
21
  * Handles writing the schema to a temp file and comparing with the snapshot.
@@ -46,6 +51,176 @@ describe('bundle', () => {
46
51
  await expectBundledSchemaToMatchSnapshot(schema, 'circular-ref-with-description.json');
47
52
  });
48
53
 
54
+ it('emits decoded internal refs for generic component names', async () => {
55
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'json-schema-ref-parser-'));
56
+
57
+ try {
58
+ const rootPath = path.join(tempDir, 'root.json');
59
+
60
+ writeJsonFile(rootPath, {
61
+ components: {
62
+ schemas: {
63
+ ClientResponse: {
64
+ properties: {
65
+ id: {
66
+ type: 'string',
67
+ },
68
+ },
69
+ type: 'object',
70
+ },
71
+ 'PaginatedListItems<ClientResponse>': {
72
+ properties: {
73
+ items: {
74
+ items: {
75
+ $ref: '#/components/schemas/ClientResponse',
76
+ },
77
+ type: 'array',
78
+ },
79
+ },
80
+ type: 'object',
81
+ },
82
+ },
83
+ },
84
+ info: {
85
+ title: 'Test API',
86
+ version: '1.0.0',
87
+ },
88
+ openapi: '3.0.0',
89
+ paths: {
90
+ '/clients': {
91
+ get: {
92
+ responses: {
93
+ '200': {
94
+ content: {
95
+ 'application/json': {
96
+ schema: {
97
+ $ref: '#/components/schemas/PaginatedListItems<ClientResponse>',
98
+ },
99
+ },
100
+ },
101
+ description: 'ok',
102
+ },
103
+ },
104
+ },
105
+ },
106
+ },
107
+ });
108
+
109
+ const refParser = new $RefParser();
110
+ const schema = (await refParser.bundle({ pathOrUrlOrSchema: rootPath })) as any;
111
+
112
+ expect(
113
+ schema.paths['/clients'].get.responses['200'].content['application/json'].schema.$ref,
114
+ ).toBe('#/components/schemas/PaginatedListItems<ClientResponse>');
115
+
116
+ const bundledJson = JSON.stringify(schema);
117
+ expect(bundledJson).not.toContain('PaginatedListItems%3CClientResponse%3E');
118
+ } finally {
119
+ fs.rmSync(tempDir, { force: true, recursive: true });
120
+ }
121
+ });
122
+
123
+ it('emits decoded refs for external schemas with generic and unicode names', async () => {
124
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'json-schema-ref-parser-'));
125
+
126
+ try {
127
+ const externalPath = path.join(tempDir, 'external.json');
128
+ const rootPath = path.join(tempDir, 'root.json');
129
+
130
+ writeJsonFile(externalPath, {
131
+ components: {
132
+ schemas: {
133
+ 'PaginatedList<ClientItem>': {
134
+ description: 'generic schema',
135
+ properties: {
136
+ next: {
137
+ $ref: '#/components/schemas/PaginatedList<ClientItem>',
138
+ },
139
+ },
140
+ type: 'object',
141
+ },
142
+ Überschrift: {
143
+ description: 'unicode schema',
144
+ properties: {
145
+ next: {
146
+ $ref: '#/components/schemas/Überschrift',
147
+ },
148
+ },
149
+ type: 'object',
150
+ },
151
+ },
152
+ },
153
+ });
154
+
155
+ writeJsonFile(rootPath, {
156
+ info: {
157
+ title: 'Test API',
158
+ version: '1.0.0',
159
+ },
160
+ openapi: '3.0.0',
161
+ paths: {
162
+ '/generic': {
163
+ get: {
164
+ responses: {
165
+ '200': {
166
+ content: {
167
+ 'application/json': {
168
+ schema: {
169
+ $ref: 'external.json#/components/schemas/PaginatedList<ClientItem>',
170
+ },
171
+ },
172
+ },
173
+ description: 'ok',
174
+ },
175
+ },
176
+ },
177
+ },
178
+ '/unicode': {
179
+ get: {
180
+ responses: {
181
+ '200': {
182
+ content: {
183
+ 'application/json': {
184
+ schema: {
185
+ $ref: 'external.json#/components/schemas/Überschrift',
186
+ },
187
+ },
188
+ },
189
+ description: 'ok',
190
+ },
191
+ },
192
+ },
193
+ },
194
+ },
195
+ });
196
+
197
+ const refParser = new $RefParser();
198
+ const schema = (await refParser.bundle({ pathOrUrlOrSchema: rootPath })) as any;
199
+ const schemas = schema.components.schemas as Record<string, any>;
200
+
201
+ const findSchemaByDescription = (description: string) =>
202
+ Object.entries(schemas).find(([, value]) => value.description === description);
203
+
204
+ const genericSchema = findSchemaByDescription('generic schema');
205
+ const unicodeSchema = findSchemaByDescription('unicode schema');
206
+
207
+ expect(genericSchema).toBeDefined();
208
+ expect(unicodeSchema).toBeDefined();
209
+
210
+ const [genericName, genericValue] = genericSchema!;
211
+ const [unicodeName, unicodeValue] = unicodeSchema!;
212
+
213
+ expect(genericValue.properties.next.$ref).toBe(`#/components/schemas/${genericName}`);
214
+ expect(unicodeValue.properties.next.$ref).toBe(`#/components/schemas/${unicodeName}`);
215
+
216
+ const bundledJson = JSON.stringify(schema);
217
+ expect(bundledJson).not.toContain('PaginatedList%3CClientItem%3E');
218
+ expect(bundledJson).not.toContain('%C3%9Cberschrift');
219
+ } finally {
220
+ fs.rmSync(tempDir, { force: true, recursive: true });
221
+ }
222
+ });
223
+
49
224
  it('bundles multiple references to the same file correctly', async () => {
50
225
  const refParser = new $RefParser();
51
226
  const pathOrUrlOrSchema = path.join(
@@ -1,9 +1,32 @@
1
1
  import path from 'node:path';
2
2
 
3
3
  import { $RefParser } from '..';
4
+ import Pointer from '../pointer';
4
5
  import { getSpecsPath } from './utils';
5
6
 
6
7
  describe('pointer', () => {
8
+ it('round-trips generic and unicode component names through join and parse', () => {
9
+ const genericRef = Pointer.join('#/components/schemas', 'PaginatedListItems<ClientResponse>');
10
+ const unicodeRef = Pointer.join('#/components/schemas', 'Überschrift');
11
+
12
+ expect(genericRef).toBe('#/components/schemas/PaginatedListItems%3CClientResponse%3E');
13
+ expect(unicodeRef).toBe('#/components/schemas/%C3%9Cberschrift');
14
+
15
+ expect(Pointer.parse(genericRef)).toEqual([
16
+ 'components',
17
+ 'schemas',
18
+ 'PaginatedListItems<ClientResponse>',
19
+ ]);
20
+ expect(Pointer.parse(unicodeRef)).toEqual(['components', 'schemas', 'Überschrift']);
21
+ });
22
+
23
+ it('preserves JSON Pointer escaping for path-like tokens while decoding them on parse', () => {
24
+ const joined = Pointer.join('#/paths', '/foo');
25
+
26
+ expect(joined).toBe('#/paths/~1foo');
27
+ expect(Pointer.parse(joined)).toEqual(['paths', '/foo']);
28
+ });
29
+
7
30
  it('inlines internal JSON Pointer refs under #/paths/ for OpenAPI bundling', async () => {
8
31
  const refParser = new $RefParser();
9
32
  const pathOrUrlOrSchema = path.join(
package/src/bundle.ts CHANGED
@@ -92,7 +92,7 @@ const getContainerTypeFromPath = (
92
92
  };
93
93
 
94
94
  /**
95
- * Inventories the given JSON Reference (i.e. records detailed information about it so we can
95
+ * Inventories the given JSON Reference (i.e., records detailed information about it so we can
96
96
  * optimize all $refs in the schema), and then crawls the resolved value.
97
97
  */
98
98
  const inventory$Ref = <S extends object = JSONSchema>({
@@ -213,10 +213,10 @@ const inventory$Ref = <S extends object = JSONSchema>({
213
213
  }
214
214
 
215
215
  const newEntry: InventoryEntry = {
216
- $ref, // The JSON Reference (e.g. {$ref: string})
217
- circular: pointer.circular, // Is this $ref pointer DIRECTLY circular? (i.e. it references itself)
216
+ $ref, // The JSON Reference (e.g., {$ref: string})
217
+ circular: pointer.circular, // Is this $ref pointer DIRECTLY circular? (i.e., it references itself)
218
218
  depth, // How far from the JSON Schema root is this $ref pointer?
219
- extended, // Does this $ref extend its resolved value? (i.e. it has extra properties, in addition to "$ref")
219
+ extended, // Does this $ref extend its resolved value? (i.e., it has extra properties, in addition to "$ref")
220
220
  external, // Does this $ref pointer point to a file other than the main JSON Schema file?
221
221
  file, // The file that the $ref pointer resolves to
222
222
  hash, // The hash within `file` that the $ref pointer resolves to
@@ -240,7 +240,7 @@ const inventory$Ref = <S extends object = JSONSchema>({
240
240
  // Recursively crawl the resolved value.
241
241
  // When the resolution followed a $ref chain to a different file,
242
242
  // use the resolved file as the base path so that local $ref values
243
- // (e.g. #/components/schemas/SiblingSchema) inside the resolved
243
+ // (e.g., #/components/schemas/SiblingSchema) inside the resolved
244
244
  // value resolve against the correct file.
245
245
  if (!existingEntry || external) {
246
246
  let crawlPath = pointer.path;
@@ -445,8 +445,8 @@ function remap(parser: $RefParser, inventory: Array<InventoryEntry>) {
445
445
  const ensureContainer = (
446
446
  type: 'schemas' | 'parameters' | 'requestBodies' | 'responses' | 'headers',
447
447
  ) => {
448
- const isOas3 = !!(root && typeof root === 'object' && typeof root.openapi === 'string');
449
- const isOas2 = !!(root && typeof root === 'object' && typeof root.swagger === 'string');
448
+ const isOas3 = Boolean(root && typeof root === 'object' && typeof root.openapi === 'string');
449
+ const isOas2 = Boolean(root && typeof root === 'object' && typeof root.swagger === 'string');
450
450
 
451
451
  if (isOas3) {
452
452
  if (!root.components || typeof root.components !== 'object') {
@@ -570,11 +570,11 @@ function remap(parser: $RefParser, inventory: Array<InventoryEntry>) {
570
570
  }
571
571
 
572
572
  // Keep internal refs internal. However, if the $ref extends the resolved value
573
- // (i.e. it has additional properties in addition to "$ref"), then we must
573
+ // (i.e., it has additional properties in addition to "$ref"), then we must
574
574
  // preserve the original $ref rather than rewriting it to the resolved hash.
575
575
  if (!entry.external) {
576
576
  if (!entry.extended && entry.$ref && typeof entry.$ref === 'object') {
577
- entry.$ref.$ref = entry.hash;
577
+ entry.$ref.$ref = decodeURI(entry.hash);
578
578
  }
579
579
  continue;
580
580
  }
@@ -582,7 +582,7 @@ function remap(parser: $RefParser, inventory: Array<InventoryEntry>) {
582
582
  // Avoid changing direct self-references; keep them internal
583
583
  if (entry.circular) {
584
584
  if (entry.$ref && typeof entry.$ref === 'object') {
585
- entry.$ref.$ref = entry.pathFromRoot;
585
+ entry.$ref.$ref = decodeURI(entry.pathFromRoot);
586
586
  }
587
587
  continue;
588
588
  }
@@ -226,9 +226,9 @@ function dereference$Ref<S extends object = JSONSchema>(
226
226
  }
227
227
 
228
228
  if (directCircular) {
229
- // The pointer is a DIRECT circular reference (i.e. it references itself).
229
+ // The pointer is a DIRECT circular reference (i.e., it references itself).
230
230
  // So replace the $ref path with the absolute path from the JSON Schema root
231
- dereferencedValue.$ref = pathFromRoot;
231
+ dereferencedValue.$ref = decodeURI(pathFromRoot);
232
232
  }
233
233
 
234
234
  const dereferencedObject = {
package/src/index.ts CHANGED
@@ -117,12 +117,12 @@ export class $RefParser {
117
117
 
118
118
  await resolveExternal(this, this.options);
119
119
  const errors = JSONParserErrorGroup.getParserErrors(this);
120
- if (errors.length > 0) {
120
+ if (errors.length) {
121
121
  throw new JSONParserErrorGroup(this);
122
122
  }
123
123
  _bundle(this, this.options);
124
124
  const errors2 = JSONParserErrorGroup.getParserErrors(this);
125
- if (errors2.length > 0) {
125
+ if (errors2.length) {
126
126
  throw new JSONParserErrorGroup(this);
127
127
  }
128
128
  return this.schema!;
@@ -148,14 +148,14 @@ export class $RefParser {
148
148
 
149
149
  await resolveExternal(this, this.options);
150
150
  const errors = JSONParserErrorGroup.getParserErrors(this);
151
- if (errors.length > 0) {
151
+ if (errors.length) {
152
152
  throw new JSONParserErrorGroup(this);
153
153
  }
154
154
  _bundle(this, this.options);
155
155
  // Merged root is ready for bundling
156
156
 
157
157
  const errors2 = JSONParserErrorGroup.getParserErrors(this);
158
- if (errors2.length > 0) {
158
+ if (errors2.length) {
159
159
  throw new JSONParserErrorGroup(this);
160
160
  }
161
161
  return this.schema!;
@@ -297,7 +297,7 @@ export class $RefParser {
297
297
 
298
298
  public mergeMany(): JSONSchema {
299
299
  const schemas = this.schemaMany || [];
300
- if (schemas.length === 0) {
300
+ if (!schemas.length) {
301
301
  throw ono('mergeMany called with no schemas. Did you run parseMany?');
302
302
  }
303
303
 
@@ -335,7 +335,7 @@ export class $RefParser {
335
335
  }
336
336
  }
337
337
  }
338
- if (Object.keys(infoAccumulator).length > 0) {
338
+ if (Object.keys(infoAccumulator).length) {
339
339
  merged.info = infoAccumulator;
340
340
  }
341
341
 
@@ -356,7 +356,7 @@ export class $RefParser {
356
356
  }
357
357
  }
358
358
  }
359
- if (servers.length > 0) {
359
+ if (servers.length) {
360
360
  merged.servers = servers;
361
361
  }
362
362
 
@@ -568,7 +568,7 @@ export class $RefParser {
568
568
  }
569
569
  }
570
570
 
571
- if (tags.length > 0) {
571
+ if (tags.length) {
572
572
  merged.tags = tags;
573
573
  }
574
574
 
@@ -585,3 +585,17 @@ export class $RefParser {
585
585
 
586
586
  export { sendRequest } from './resolvers/url';
587
587
  export type { JSONSchema } from './types';
588
+ export type { JSONParserErrorType } from './util/errors';
589
+ export {
590
+ InvalidPointerError,
591
+ isHandledError,
592
+ JSONParserError,
593
+ JSONParserErrorGroup,
594
+ MissingPointerError,
595
+ normalizeError,
596
+ ParserError,
597
+ ResolverError,
598
+ TimeoutError,
599
+ UnmatchedParserError,
600
+ UnmatchedResolverError,
601
+ } from './util/errors';
@@ -1,5 +1,4 @@
1
- import yaml from 'js-yaml';
2
- import { JSON_SCHEMA } from 'js-yaml';
1
+ import { parse } from 'yaml';
3
2
 
4
3
  import type { FileInfo, JSONSchema, Plugin } from '../types';
5
4
  import { ParserError } from '../util/errors';
@@ -16,8 +15,7 @@ export const yamlParser: Plugin = {
16
15
  }
17
16
 
18
17
  try {
19
- const yamlSchema = yaml.load(data, { schema: JSON_SCHEMA }) as JSONSchema;
20
- return yamlSchema;
18
+ return parse(data) as JSONSchema;
21
19
  } catch (error: any) {
22
20
  throw new ParserError(error?.message || 'Parser Error', file.url);
23
21
  }
package/src/pointer.ts CHANGED
@@ -140,7 +140,7 @@ class Pointer<S extends object = JSONSchema> {
140
140
  }
141
141
  }
142
142
 
143
- if (errors.length > 0) {
143
+ if (errors.length) {
144
144
  throw errors.length === 1
145
145
  ? errors[0]
146
146
  : new AggregateError(errors, 'Multiple missing pointer errors');
@@ -171,7 +171,7 @@ class Pointer<S extends object = JSONSchema> {
171
171
  const tokens = Pointer.parse(this.path);
172
172
  let token;
173
173
 
174
- if (tokens.length === 0) {
174
+ if (!tokens.length) {
175
175
  // There are no tokens, replace the entire object with the new value
176
176
  this.value = value;
177
177
  return value;
@@ -205,7 +205,7 @@ class Pointer<S extends object = JSONSchema> {
205
205
  /**
206
206
  * Parses a JSON pointer (or a path containing a JSON pointer in the hash)
207
207
  * and returns an array of the pointer's tokens.
208
- * (e.g. "schema.json#/definitions/person/name" => ["definitions", "person", "name"])
208
+ * (e.g., "schema.json#/definitions/person/name" => ["definitions", "person", "name"])
209
209
  *
210
210
  * The pointer is parsed according to RFC 6901
211
211
  * {@link https://tools.ietf.org/html/rfc6901#section-3}
@@ -244,8 +244,8 @@ class Pointer<S extends object = JSONSchema> {
244
244
  /**
245
245
  * Creates a JSON pointer path, by joining one or more tokens to a base path.
246
246
  *
247
- * @param base - The base path (e.g. "schema.json#/definitions/person")
248
- * @param tokens - The token(s) to append (e.g. ["name", "first"])
247
+ * @param base - The base path (e.g., "schema.json#/definitions/person")
248
+ * @param tokens - The token(s) to append (e.g., ["name", "first"])
249
249
  * @returns
250
250
  */
251
251
  static join(base: string, tokens: string | string[]) {
package/src/ref.ts CHANGED
@@ -46,7 +46,7 @@ class $Ref<S extends object = JSONSchema> {
46
46
  $refs: $Refs<S>;
47
47
 
48
48
  /**
49
- * Indicates the type of {@link $Ref#path} (e.g. "file", "http", etc.)
49
+ * Indicates the type of {@link $Ref#path} (e.g., "file", "http", etc.)
50
50
  */
51
51
  pathType: string | unknown;
52
52
 
@@ -152,7 +152,7 @@ class $Ref<S extends object = JSONSchema> {
152
152
  value !== null &&
153
153
  '$ref' in value &&
154
154
  typeof value.$ref === 'string' &&
155
- value.$ref.length > 0
155
+ Boolean(value.$ref.length)
156
156
  );
157
157
  }
158
158
 
package/src/refs.ts CHANGED
@@ -224,7 +224,7 @@ function getPaths<S extends object = JSONSchema>($refs: $RefsMap<S>, types: stri
224
224
 
225
225
  // Filter the paths by type
226
226
  types = Array.isArray(types[0]) ? types[0] : Array.prototype.slice.call(types);
227
- if (types.length > 0 && types[0]) {
227
+ if (types.length && types[0]) {
228
228
  paths = paths.filter((key) => types.includes($refs[key]!.pathType as string));
229
229
  }
230
230