Skip to content

Commit b0bb1a6

Browse files
refactor: create json schema ref resolver (#507)
* refactor: create json schema ref resolver * refactor: add separate Ajv schema existence check
1 parent ddb1c48 commit b0bb1a6

File tree

4 files changed

+127
-11
lines changed

4 files changed

+127
-11
lines changed

index.js

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const { randomUUID } = require('crypto')
99

1010
const validate = require('./schema-validator')
1111
const Serializer = require('./serializer')
12+
const RefResolver = require('./ref-resolver')
1213
const buildAjv = require('./ajv')
1314

1415
let largeArraySize = 2e4
@@ -57,20 +58,12 @@ function resolveRef (location, ref) {
5758
const schemaId = ref.slice(0, hashIndex) || location.schemaId
5859
const jsonPointer = ref.slice(hashIndex) || '#'
5960

60-
const schemaRef = schemaId + jsonPointer
61+
const schema = refResolver.getSchema(schemaId, jsonPointer)
6162

62-
let ajvSchema
63-
try {
64-
ajvSchema = ajvInstance.getSchema(schemaRef)
65-
} catch (error) {
63+
if (schema === undefined) {
6664
throw new Error(`Cannot find reference "${ref}"`)
6765
}
6866

69-
if (ajvSchema === undefined) {
70-
throw new Error(`Cannot find reference "${ref}"`)
71-
}
72-
73-
const schema = ajvSchema.schema
7467
if (schema.$ref !== undefined) {
7568
return resolveRef({ schema, schemaId, jsonPointer }, schema.$ref)
7669
}
@@ -83,6 +76,7 @@ const objectReferenceSerializersMap = new Map()
8376

8477
let rootSchemaId = null
8578
let ajvInstance = null
79+
let refResolver = null
8680
let contextFunctions = null
8781

8882
function build (schema, options) {
@@ -95,11 +89,13 @@ function build (schema, options) {
9589
options = options || {}
9690

9791
ajvInstance = buildAjv(options.ajv)
92+
refResolver = new RefResolver()
9893
rootSchemaId = schema.$id || randomUUID()
9994

10095
isValidSchema(schema)
10196
extendDateTimeType(schema)
10297
ajvInstance.addSchema(schema, rootSchemaId)
98+
refResolver.addSchema(schema, rootSchemaId)
10399

104100
if (options.schema) {
105101
const externalSchemas = clone(options.schema)
@@ -114,7 +110,14 @@ function build (schema, options) {
114110
schemaKey = key + externalSchema.$id // relative URI
115111
}
116112

117-
if (ajvInstance.getSchema(schemaKey) === undefined) {
113+
if (refResolver.getSchema(schemaKey) === undefined) {
114+
refResolver.addSchema(externalSchema, key)
115+
}
116+
117+
if (
118+
ajvInstance.refs[schemaKey] === undefined &&
119+
ajvInstance.schemas[schemaKey] === undefined
120+
) {
118121
ajvInstance.addSchema(externalSchema, schemaKey)
119122
}
120123
}
@@ -178,6 +181,7 @@ function build (schema, options) {
178181
const stringifyFunc = contextFunc(ajvInstance, serializer)
179182

180183
ajvInstance = null
184+
refResolver = null
181185
rootSchemaId = null
182186
contextFunctions = null
183187
arrayItemsReferenceSerializersMap.clear()
@@ -494,6 +498,7 @@ function mergeAllOfSchema (location, schema, mergedSchema) {
494498

495499
mergedSchema.$id = `merged_${randomUUID()}`
496500
ajvInstance.addSchema(mergedSchema)
501+
refResolver.addSchema(mergedSchema)
497502
location.schemaId = mergedSchema.$id
498503
location.jsonPointer = '#'
499504
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@fastify/deepmerge": "^1.0.0",
5353
"ajv": "^8.10.0",
5454
"ajv-formats": "^2.1.1",
55+
"fast-deep-equal": "^3.1.3",
5556
"fast-uri": "^2.1.0",
5657
"rfdc": "^1.2.0"
5758
},

ref-resolver.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use strict'
2+
3+
const deepEqual = require('fast-deep-equal')
4+
5+
class RefResolver {
6+
constructor () {
7+
this.schemas = {}
8+
}
9+
10+
addSchema (schema, schemaId) {
11+
if (schema.$id !== undefined && schema.$id.charAt(0) !== '#') {
12+
schemaId = schema.$id
13+
}
14+
this.insertSchemaBySchemaId(schema, schemaId)
15+
this.insertSchemaSubschemas(schema, schemaId)
16+
}
17+
18+
getSchema (schemaId, jsonPointer = '#') {
19+
const schema = this.schemas[schemaId]
20+
if (schema === undefined) {
21+
return undefined
22+
}
23+
if (schema.anchors[jsonPointer] !== undefined) {
24+
return schema.anchors[jsonPointer]
25+
}
26+
return getDataByJSONPointer(schema.schema, jsonPointer)
27+
}
28+
29+
insertSchemaBySchemaId (schema, schemaId) {
30+
if (
31+
this.schemas[schemaId] !== undefined &&
32+
!deepEqual(schema, this.schemas[schemaId].schema)
33+
) {
34+
throw new Error(`There is already another schema with id ${schemaId}`)
35+
}
36+
this.schemas[schemaId] = { schema, anchors: {} }
37+
}
38+
39+
insertSchemaByAnchor (schema, schemaId, anchor) {
40+
const { anchors } = this.schemas[schemaId]
41+
if (
42+
anchors[anchor] !== undefined &&
43+
!deepEqual(schema, anchors[anchor])
44+
) {
45+
throw new Error(`There is already another schema with id ${schemaId}#${anchor}`)
46+
}
47+
anchors[anchor] = schema
48+
}
49+
50+
insertSchemaSubschemas (schema, rootSchemaId) {
51+
const schemaId = schema.$id
52+
if (schemaId !== undefined && typeof schemaId === 'string') {
53+
if (schemaId.charAt(0) === '#') {
54+
this.insertSchemaByAnchor(schema, rootSchemaId, schemaId)
55+
} else {
56+
this.insertSchemaBySchemaId(schema, schemaId)
57+
rootSchemaId = schemaId
58+
}
59+
}
60+
61+
for (const key in schema) {
62+
if (typeof schema[key] === 'object' && schema[key] !== null) {
63+
this.insertSchemaSubschemas(schema[key], rootSchemaId)
64+
}
65+
}
66+
}
67+
}
68+
69+
function getDataByJSONPointer (data, jsonPointer) {
70+
const parts = jsonPointer.split('/')
71+
let current = data
72+
for (const part of parts) {
73+
if (part === '' || part === '#') continue
74+
if (typeof current !== 'object' || current === null) {
75+
return undefined
76+
}
77+
current = current[part]
78+
}
79+
return current
80+
}
81+
82+
module.exports = RefResolver

test/ref.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1953,3 +1953,31 @@ test('should resolve absolute $refs', (t) => {
19531953

19541954
t.equal(output, JSON.stringify(object))
19551955
})
1956+
1957+
test('nested schema should overwrite anchor scope', (t) => {
1958+
t.plan(2)
1959+
1960+
const externalSchema = {
1961+
root: {
1962+
$id: 'root',
1963+
definitions: {
1964+
subschema: {
1965+
$id: 'subschema',
1966+
definitions: {
1967+
anchorSchema: {
1968+
$id: '#anchor',
1969+
type: 'string'
1970+
}
1971+
}
1972+
}
1973+
}
1974+
}
1975+
}
1976+
1977+
const data = 'test'
1978+
const stringify = build({ $ref: 'subschema#anchor' }, { schema: externalSchema })
1979+
const output = stringify(data)
1980+
1981+
t.equal(output, JSON.stringify(data))
1982+
t.throws(() => build({ $ref: 'root#anchor' }, { schema: externalSchema }))
1983+
})

0 commit comments

Comments
 (0)