Skip to content

Commit e979b78

Browse files
committed
feat: bring json-schema-ref-parser in-house
1 parent 778a00d commit e979b78

44 files changed

Lines changed: 4095 additions & 29 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/slick-queens-buy.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hey-api/json-schema-ref-parser": minor
3+
---
4+
5+
**feat**: clean up dependencies

dev/inputs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const inputs = {
99
opencode: path.resolve(getSpecsPath(), '3.1.x', 'opencode.yaml'),
1010
petstore:
1111
'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml',
12+
redfish: 'http://redfish.dmtf.org/schemas/v1/Message.v1_2_1.yaml',
1213
scalar: 'scalar:@scalar/access-service',
1314
transformers: path.resolve(getSpecsPath(), '3.1.x', 'transformers.json'),
1415
validators: path.resolve(getSpecsPath(), '3.1.x', 'validators.yaml'),
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# JSON Schema $Ref Parser
2+
3+
#### Parse, Resolve, and Dereference JSON Schema $ref pointers
4+
5+
## Installation
6+
7+
Install using [npm](https://docs.npmjs.com/about-npm/):
8+
9+
```bash
10+
npm install @hey-api/json-schema-ref-parser
11+
yarn add @hey-api/json-schema-ref-parser
12+
bun add @hey-api/json-schema-ref-parser
13+
```
14+
15+
## The Problem:
16+
17+
You've got a JSON Schema with `$ref` pointers to other files and/or URLs. Maybe you know all the referenced files ahead
18+
of time. Maybe you don't. Maybe some are local files, and others are remote URLs. Maybe they are a mix of JSON and YAML
19+
format. Maybe some of the files contain cross-references to each other.
20+
21+
```json
22+
{
23+
"definitions": {
24+
"person": {
25+
// references an external file
26+
"$ref": "schemas/people/Bruce-Wayne.json"
27+
},
28+
"place": {
29+
// references a sub-schema in an external file
30+
"$ref": "schemas/places.yaml#/definitions/Gotham-City"
31+
},
32+
"thing": {
33+
// references a URL
34+
"$ref": "http://wayne-enterprises.com/things/batmobile"
35+
},
36+
"color": {
37+
// references a value in an external file via an internal reference
38+
"$ref": "#/definitions/thing/properties/colors/black-as-the-night"
39+
}
40+
}
41+
}
42+
```
43+
44+
## The Solution:
45+
46+
JSON Schema $Ref Parser is a full [JSON Reference](https://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03)
47+
and [JSON Pointer](https://tools.ietf.org/html/rfc6901) implementation that crawls even the most
48+
complex [JSON Schemas](http://json-schema.org/latest/json-schema-core.html) and gives you simple, straightforward
49+
JavaScript objects.
50+
51+
- Use **JSON** or **YAML** schemas — or even a mix of both!
52+
- Supports `$ref` pointers to external files and URLs, as well as custom sources such as databases
53+
- Can bundle multiple files into a single schema that only has _internal_ `$ref` pointers
54+
- Can dereference your schema, producing a plain-old JavaScript object that's easy to work with
55+
- Supports circular references, nested references,
56+
back-references, and cross-references between files
57+
- Maintains object reference equality — `$ref` pointers to the same value always resolve to the same object
58+
instance
59+
- Compatible with Node LTS and beyond, and all major web browsers on Windows, Mac, and Linux
60+
61+
## Example
62+
63+
```javascript
64+
import { $RefParser } from '@hey-api/json-schema-ref-parser';
65+
66+
try {
67+
const parser = new $RefParser();
68+
await parser.dereference({ pathOrUrlOrSchema: mySchema });
69+
console.log(parser.schema.definitions.person.properties.firstName);
70+
} catch (err) {
71+
console.error(err);
72+
}
73+
```
74+
75+
### New in this fork (@hey-api)
76+
77+
- **Multiple inputs with `bundleMany`**: Merge and bundle several OpenAPI/JSON Schema inputs (files, URLs, or raw objects) into a single schema. Components are prefixed to avoid name collisions, paths are namespaced on conflict, and `$ref`s are rewritten accordingly.
78+
79+
```javascript
80+
import { $RefParser } from '@hey-api/json-schema-ref-parser';
81+
82+
const parser = new $RefParser();
83+
const merged = await parser.bundleMany({
84+
pathOrUrlOrSchemas: [
85+
'./specs/a.yaml',
86+
'https://example.com/b.yaml',
87+
{ openapi: '3.1.0', info: { title: 'Inline' }, paths: {} },
88+
],
89+
});
90+
91+
// merged.components.* will contain prefixed names like a_<name>, b_<name>, etc.
92+
```
93+
94+
- **Dereference hooks**: Fine-tune dereferencing with `excludedPathMatcher(path) => boolean` to skip subpaths and `onDereference(path, value, parent, parentPropName)` to observe replacements.
95+
96+
```javascript
97+
const parser = new $RefParser();
98+
parser.options.dereference.excludedPathMatcher = (p) => p.includes('/example/');
99+
parser.options.dereference.onDereference = (p, v) => {
100+
// inspect p / v as needed
101+
};
102+
await parser.dereference({ pathOrUrlOrSchema: './openapi.yaml' });
103+
```
104+
105+
- **Smart input resolution**: You can pass a file path, URL, or raw schema object. If a raw schema includes `$id`, it is used as the base URL for resolving relative `$ref`s.
106+
107+
```javascript
108+
await new $RefParser().bundle({
109+
pathOrUrlOrSchema: {
110+
$id: 'https://api.example.com/openapi.json',
111+
openapi: '3.1.0',
112+
paths: {
113+
'/ping': { get: { responses: { 200: { description: 'ok' } } } },
114+
},
115+
},
116+
});
117+
```
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"name": "@hey-api/json-schema-ref-parser",
3+
"version": "1.2.4",
4+
"description": "Parse, Resolve, and Dereference JSON Schema $ref pointers",
5+
"keywords": [
6+
"$ref",
7+
"dereference",
8+
"json",
9+
"json-pointer",
10+
"json-schema",
11+
"jsonschema",
12+
"resolve",
13+
"schema"
14+
],
15+
"homepage": "https://heyapi.dev/",
16+
"bugs": {
17+
"url": "https://github.com/hey-api/openapi-ts/issues"
18+
},
19+
"license": "MIT",
20+
"author": {
21+
"name": "Hey API",
22+
"email": "lubos@heyapi.dev",
23+
"url": "https://heyapi.dev"
24+
},
25+
"repository": {
26+
"type": "git",
27+
"url": "git+https://github.com/hey-api/openapi-ts.git"
28+
},
29+
"funding": "https://github.com/sponsors/hey-api",
30+
"files": [
31+
"src",
32+
"dist",
33+
"cjs"
34+
],
35+
"type": "module",
36+
"main": "./dist/index.mjs",
37+
"types": "./dist/index.d.mts",
38+
"exports": {
39+
".": {
40+
"types": "./dist/index.d.mts",
41+
"import": "./dist/index.mjs"
42+
},
43+
"./package.json": "./package.json"
44+
},
45+
"scripts": {
46+
"build": "tsdown && pnpm check-exports",
47+
"check-exports": "attw --pack . --profile esm-only --ignore-rules cjs-resolves-to-esm",
48+
"dev": "tsdown --watch",
49+
"prepublishOnly": "pnpm build",
50+
"typecheck": "tsc --noEmit"
51+
},
52+
"dependencies": {
53+
"@jsdevtools/ono": "7.1.3",
54+
"@types/json-schema": "7.0.15",
55+
"js-yaml": "4.1.1"
56+
},
57+
"devDependencies": {
58+
"@types/js-yaml": "4.0.9",
59+
"typescript": "5.9.3"
60+
},
61+
"engines": {
62+
"node": ">=20.19.0"
63+
}
64+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import path from 'node:path';
2+
3+
import { $RefParser } from '..';
4+
import { getSpecsPath } from './utils';
5+
6+
describe('bundle', () => {
7+
it('handles circular reference with description', async () => {
8+
const refParser = new $RefParser();
9+
const pathOrUrlOrSchema = path.join(
10+
getSpecsPath(),
11+
'json-schema-ref-parser',
12+
'circular-ref-with-description.json',
13+
);
14+
const schema = await refParser.bundle({ pathOrUrlOrSchema });
15+
expect(schema).toEqual({
16+
schemas: {
17+
Bar: {
18+
$ref: '#/schemas/Foo',
19+
description: 'ok',
20+
},
21+
Foo: {
22+
$ref: '#/schemas/Bar',
23+
},
24+
},
25+
});
26+
});
27+
28+
it('bundles multiple references to the same file correctly', async () => {
29+
const refParser = new $RefParser();
30+
const pathOrUrlOrSchema = path.join(
31+
getSpecsPath(),
32+
'json-schema-ref-parser',
33+
'multiple-refs.json',
34+
);
35+
const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any;
36+
37+
// Both parameters should now be $ref to the same internal definition
38+
const firstParam = schema.paths['/test1/{pathId}'].get.parameters[0];
39+
const secondParam = schema.paths['/test2/{pathId}'].get.parameters[0];
40+
41+
// The $ref should match the output structure in file_context_0
42+
expect(firstParam.$ref).toBe('#/components/parameters/path-parameter_pathId');
43+
expect(secondParam.$ref).toBe('#/components/parameters/path-parameter_pathId');
44+
45+
// The referenced parameter should exist and match the expected structure
46+
expect(schema.components).toBeDefined();
47+
expect(schema.components.parameters).toBeDefined();
48+
expect(schema.components.parameters['path-parameter_pathId']).toEqual({
49+
in: 'path',
50+
name: 'pathId',
51+
required: true,
52+
schema: {
53+
description: 'Unique identifier for the path',
54+
format: 'uuid',
55+
type: 'string',
56+
},
57+
});
58+
});
59+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import path from 'node:path';
2+
3+
import { getResolvedInput } from '../index';
4+
5+
describe('getResolvedInput', () => {
6+
it('handles url', async () => {
7+
const pathOrUrlOrSchema = 'https://foo.com';
8+
const resolvedInput = await getResolvedInput({ pathOrUrlOrSchema });
9+
expect(resolvedInput.type).toBe('url');
10+
expect(resolvedInput.schema).toBeUndefined();
11+
expect(resolvedInput.path).toBe('https://foo.com/');
12+
});
13+
14+
it('handles file', async () => {
15+
const pathOrUrlOrSchema = './path/to/openapi.json';
16+
const resolvedInput = await getResolvedInput({ pathOrUrlOrSchema });
17+
expect(resolvedInput.type).toBe('file');
18+
expect(resolvedInput.schema).toBeUndefined();
19+
expect(path.normalize(resolvedInput.path).toLowerCase()).toBe(
20+
path.normalize(path.resolve('./path/to/openapi.json')).toLowerCase(),
21+
);
22+
});
23+
24+
it('handles raw spec', async () => {
25+
const pathOrUrlOrSchema = {
26+
info: {
27+
version: '1.0.0',
28+
},
29+
openapi: '3.1.0',
30+
paths: {},
31+
};
32+
const resolvedInput = await getResolvedInput({ pathOrUrlOrSchema });
33+
expect(resolvedInput.type).toBe('json');
34+
expect(resolvedInput.schema).toEqual({
35+
info: {
36+
version: '1.0.0',
37+
},
38+
openapi: '3.1.0',
39+
paths: {},
40+
});
41+
expect(resolvedInput.path).toBe('');
42+
});
43+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import path from 'node:path';
2+
3+
import { $RefParser } from '..';
4+
import { getSpecsPath } from './utils';
5+
6+
describe('pointer', () => {
7+
it('inlines internal JSON Pointer refs under #/paths/ for OpenAPI bundling', async () => {
8+
const refParser = new $RefParser();
9+
const pathOrUrlOrSchema = path.join(
10+
getSpecsPath(),
11+
'json-schema-ref-parser',
12+
'openapi-paths-ref.json',
13+
);
14+
const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any;
15+
16+
// The GET endpoint should have its schema defined inline
17+
const getSchema = schema.paths['/foo'].get.responses['200'].content['application/json'].schema;
18+
expect(getSchema.$ref).toBeUndefined();
19+
expect(getSchema.type).toBe('object');
20+
expect(getSchema.properties.bar.type).toBe('string');
21+
22+
// The POST endpoint should have its schema inlined (copied) instead of a $ref
23+
const postSchema =
24+
schema.paths['/foo'].post.responses['200'].content['application/json'].schema;
25+
expect(postSchema.$ref).toBe(
26+
'#/paths/~1foo/get/responses/200/content/application~1json/schema',
27+
);
28+
expect(postSchema.type).toBeUndefined();
29+
expect(postSchema.properties?.bar?.type).toBeUndefined();
30+
31+
// Both schemas should be identical objects
32+
expect(postSchema).not.toBe(getSchema);
33+
});
34+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import path from 'node:path';
2+
3+
export const getSpecsPath = (): string => path.join(__dirname, '..', '..', '..', '..', 'specs');

0 commit comments

Comments
 (0)