Skip to content

Commit bb4a733

Browse files
committed
Handle true property value
A JSON Schema of `true` means to allow anything. It can be useful when extending closed schemas; see https://json-schema.org/understanding-json-schema/reference/object#extending. Fixes #1643
1 parent 3cedb15 commit bb4a733

File tree

2 files changed

+123
-61
lines changed

2 files changed

+123
-61
lines changed

src/js/Node.js

Lines changed: 88 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4486,95 +4486,122 @@ Node._findEnum = schema => {
44864486
}
44874487

44884488
/**
4489-
* Return the part of a JSON schema matching given path.
4489+
* Implementation for _findSchema
44904490
* @param {Object} topLevelSchema
44914491
* @param {Object} schemaRefs
44924492
* @param {Array.<string | number>} path
44934493
* @param {Object} currentSchema
4494-
* @return {Object | null}
4494+
* @return {Object | boolean | null}
44954495
* @private
44964496
*/
4497-
Node._findSchema = (topLevelSchema, schemaRefs, path, currentSchema = topLevelSchema) => {
4497+
Node._findOneSchema = (topLevelSchema, schemaRefs, path, currentSchema) => {
44984498
const nextPath = path.slice(1, path.length)
44994499
const nextKey = path[0]
45004500

4501-
let possibleSchemas = [currentSchema]
4502-
for (const subSchemas of [currentSchema.oneOf, currentSchema.anyOf, currentSchema.allOf]) {
4503-
if (Array.isArray(subSchemas)) {
4504-
possibleSchemas = possibleSchemas.concat(subSchemas)
4505-
}
4506-
}
4507-
4508-
for (const schema of possibleSchemas) {
4509-
currentSchema = schema
4510-
4511-
if ('$ref' in currentSchema && typeof currentSchema.$ref === 'string') {
4512-
const ref = currentSchema.$ref
4513-
if (ref in schemaRefs) {
4514-
currentSchema = schemaRefs[ref]
4515-
} else if (ref.startsWith('#/')) {
4516-
const refPath = ref.substring(2).split('/')
4517-
currentSchema = topLevelSchema
4518-
for (const segment of refPath) {
4519-
if (segment in currentSchema) {
4520-
currentSchema = currentSchema[segment]
4521-
} else {
4522-
throw Error(`Unable to resolve reference ${ref}`)
4523-
}
4524-
}
4525-
} else if (ref.match(/#\//g)?.length === 1) {
4526-
const [schemaUrl, relativePath] = ref.split('#/')
4527-
if (schemaUrl in schemaRefs) {
4528-
const referencedSchema = schemaRefs[schemaUrl]
4529-
const reference = { $ref: '#/'.concat(relativePath) }
4530-
const auxNextPath = []
4531-
auxNextPath.push(nextKey)
4532-
if (nextPath.length > 0) {
4533-
auxNextPath.push(...nextPath)
4534-
}
4535-
return Node._findSchema(referencedSchema, schemaRefs, auxNextPath, reference)
4501+
if (typeof currentSchema === 'object' && '$ref' in currentSchema && typeof currentSchema.$ref === 'string') {
4502+
const ref = currentSchema.$ref
4503+
if (ref in schemaRefs) {
4504+
currentSchema = schemaRefs[ref]
4505+
} else if (ref.startsWith('#/')) {
4506+
const refPath = ref.substring(2).split('/')
4507+
currentSchema = topLevelSchema
4508+
for (const segment of refPath) {
4509+
if (segment in currentSchema) {
4510+
currentSchema = currentSchema[segment]
45364511
} else {
45374512
throw Error(`Unable to resolve reference ${ref}`)
45384513
}
4514+
}
4515+
} else if (ref.match(/#\//g)?.length === 1) {
4516+
const [schemaUrl, relativePath] = ref.split('#/')
4517+
if (schemaUrl in schemaRefs) {
4518+
const referencedSchema = schemaRefs[schemaUrl]
4519+
const reference = { $ref: '#/'.concat(relativePath) }
4520+
const auxNextPath = []
4521+
auxNextPath.push(nextKey)
4522+
if (nextPath.length > 0) {
4523+
auxNextPath.push(...nextPath)
4524+
}
4525+
return Node._findSchema(referencedSchema, schemaRefs, auxNextPath, reference)
45394526
} else {
45404527
throw Error(`Unable to resolve reference ${ref}`)
45414528
}
4529+
} else {
4530+
throw Error(`Unable to resolve reference ${ref}`)
45424531
}
4532+
}
45434533

4544-
// We have no more path segments to resolve, return the currently found schema
4545-
// We do this here, after resolving references, in case of the leaf schema beeing a reference
4546-
if (nextKey === undefined) {
4547-
return currentSchema
4548-
}
4534+
// We have no more path segments to resolve, return the currently found schema
4535+
// We do this here, after resolving references, in case of the leaf schema beeing a reference
4536+
if (nextKey === undefined) {
4537+
return currentSchema
4538+
}
45494539

4550-
if (typeof nextKey === 'string') {
4551-
if (typeof currentSchema.properties === 'object' && currentSchema.properties !== null && nextKey in currentSchema.properties) {
4552-
currentSchema = currentSchema.properties[nextKey]
4553-
return Node._findSchema(topLevelSchema, schemaRefs, nextPath, currentSchema)
4554-
}
4555-
if (typeof currentSchema.patternProperties === 'object' && currentSchema.patternProperties !== null) {
4556-
for (const prop in currentSchema.patternProperties) {
4557-
if (nextKey.match(prop)) {
4558-
currentSchema = currentSchema.patternProperties[prop]
4559-
return Node._findSchema(topLevelSchema, schemaRefs, nextPath, currentSchema)
4560-
}
4540+
if (typeof nextKey === 'string') {
4541+
if (typeof currentSchema.properties === 'object' && currentSchema.properties !== null && nextKey in currentSchema.properties) {
4542+
currentSchema = currentSchema.properties[nextKey]
4543+
return Node._findSchema(topLevelSchema, schemaRefs, nextPath, currentSchema)
4544+
}
4545+
if (typeof currentSchema.patternProperties === 'object' && currentSchema.patternProperties !== null) {
4546+
for (const prop in currentSchema.patternProperties) {
4547+
if (nextKey.match(prop)) {
4548+
currentSchema = currentSchema.patternProperties[prop]
4549+
return Node._findSchema(topLevelSchema, schemaRefs, nextPath, currentSchema)
45614550
}
45624551
}
4563-
if (typeof currentSchema.additionalProperties === 'object') {
4564-
currentSchema = currentSchema.additionalProperties
4565-
return Node._findSchema(topLevelSchema, schemaRefs, nextPath, currentSchema)
4566-
}
4567-
continue
45684552
}
4569-
if (typeof nextKey === 'number' && typeof currentSchema.items === 'object' && currentSchema.items !== null) {
4570-
currentSchema = currentSchema.items
4553+
if (typeof currentSchema.additionalProperties === 'object') {
4554+
currentSchema = currentSchema.additionalProperties
45714555
return Node._findSchema(topLevelSchema, schemaRefs, nextPath, currentSchema)
45724556
}
4557+
return null
4558+
}
4559+
if (typeof nextKey === 'number' && typeof currentSchema.items === 'object' && currentSchema.items !== null) {
4560+
currentSchema = currentSchema.items
4561+
return Node._findSchema(topLevelSchema, schemaRefs, nextPath, currentSchema)
45734562
}
45744563

45754564
return null
45764565
}
45774566

4567+
/**
4568+
* Return the part of a JSON schema matching given path.
4569+
*
4570+
* Note that this attempts to find *a* schema matching the path, not necessarily
4571+
* the best / most appropriate. For example, oneOf vs. anyOf vs. allOf may
4572+
* result in different schemas being applied in practice.
4573+
*
4574+
* @param {Object} topLevelSchema
4575+
* @param {Object} schemaRefs
4576+
* @param {Array.<string | number>} path
4577+
* @param {Object} currentSchema
4578+
* @return {Object | null}
4579+
* @private
4580+
*/
4581+
Node._findSchema = (topLevelSchema, schemaRefs, path, currentSchema = topLevelSchema) => {
4582+
let possibleSchemas = [currentSchema]
4583+
for (const subSchemas of [currentSchema.oneOf, currentSchema.anyOf, currentSchema.allOf]) {
4584+
if (Array.isArray(subSchemas)) {
4585+
possibleSchemas = possibleSchemas.concat(subSchemas)
4586+
}
4587+
}
4588+
4589+
let fallback = null
4590+
for (const schema of possibleSchemas) {
4591+
const result = Node._findOneSchema(topLevelSchema, schemaRefs, path, schema)
4592+
// Although we don't attempt to find the best / most appropriate schema, we
4593+
// can at least attempt to find something more specific than `true`.
4594+
if (result === true) {
4595+
fallback = true
4596+
continue
4597+
} else if (result !== null) {
4598+
return result
4599+
}
4600+
}
4601+
4602+
return fallback
4603+
}
4604+
45784605
/**
45794606
* Remove nodes
45804607
* @param {Node[] | Node} nodes

test/Node.test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,40 @@ describe('Node', () => {
198198
})
199199
})
200200

201+
// https://json-schema.org/understanding-json-schema/reference/object#extending
202+
it('works with extending schemas', () => {
203+
const schema = {
204+
properties: {
205+
name: true,
206+
manager: {
207+
type: 'string',
208+
enum: ['c', 'd']
209+
}
210+
},
211+
additionalProperties: false,
212+
allOf: [
213+
{
214+
properties: {
215+
name: {
216+
type: 'string',
217+
enum: ['a', 'b']
218+
}
219+
}
220+
}
221+
]
222+
}
223+
let path = ['name']
224+
assert.deepStrictEqual(Node._findSchema(schema, {}, path), {
225+
type: 'string',
226+
enum: ['a', 'b']
227+
})
228+
path = ['manager']
229+
assert.deepStrictEqual(Node._findSchema(schema, {}, path), {
230+
type: 'string',
231+
enum: ['c', 'd']
232+
})
233+
})
234+
201235
describe('with $ref', () => {
202236
it('should find a referenced schema', () => {
203237
const schema = {
@@ -401,6 +435,7 @@ describe('Node', () => {
401435
assert.notStrictEqual(Node._findSchema(schema, { 'definitions.json': definitions }, path), foundSchema)
402436
})
403437
})
438+
404439
describe('with pattern properties', () => {
405440
it('should find schema', () => {
406441
const schema = {

0 commit comments

Comments
 (0)