Skip to content

Commit

Permalink
fix: handle true property value in a JSON Schema (#1644)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
joshkel authored Feb 17, 2025
1 parent e8628b9 commit 2fdbc13
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 61 deletions.
149 changes: 88 additions & 61 deletions src/js/Node.js
Original file line number Diff line number Diff line change
Expand Up @@ -4486,95 +4486,122 @@ Node._findEnum = schema => {
}

/**
* Return the part of a JSON schema matching given path.
* Implementation for _findSchema
* @param {Object} topLevelSchema
* @param {Object} schemaRefs
* @param {Array.<string | number>} path
* @param {Object} currentSchema
* @return {Object | null}
* @return {Object | boolean | null}
* @private
*/
Node._findSchema = (topLevelSchema, schemaRefs, path, currentSchema = topLevelSchema) => {
Node._findOneSchema = (topLevelSchema, schemaRefs, path, currentSchema) => {
const nextPath = path.slice(1, path.length)
const nextKey = path[0]

let possibleSchemas = [currentSchema]
for (const subSchemas of [currentSchema.oneOf, currentSchema.anyOf, currentSchema.allOf]) {
if (Array.isArray(subSchemas)) {
possibleSchemas = possibleSchemas.concat(subSchemas)
}
}

for (const schema of possibleSchemas) {
currentSchema = schema

if ('$ref' in currentSchema && typeof currentSchema.$ref === 'string') {
const ref = currentSchema.$ref
if (ref in schemaRefs) {
currentSchema = schemaRefs[ref]
} else if (ref.startsWith('#/')) {
const refPath = ref.substring(2).split('/')
currentSchema = topLevelSchema
for (const segment of refPath) {
if (segment in currentSchema) {
currentSchema = currentSchema[segment]
} else {
throw Error(`Unable to resolve reference ${ref}`)
}
}
} else if (ref.match(/#\//g)?.length === 1) {
const [schemaUrl, relativePath] = ref.split('#/')
if (schemaUrl in schemaRefs) {
const referencedSchema = schemaRefs[schemaUrl]
const reference = { $ref: '#/'.concat(relativePath) }
const auxNextPath = []
auxNextPath.push(nextKey)
if (nextPath.length > 0) {
auxNextPath.push(...nextPath)
}
return Node._findSchema(referencedSchema, schemaRefs, auxNextPath, reference)
if (typeof currentSchema === 'object' && '$ref' in currentSchema && typeof currentSchema.$ref === 'string') {
const ref = currentSchema.$ref
if (ref in schemaRefs) {
currentSchema = schemaRefs[ref]
} else if (ref.startsWith('#/')) {
const refPath = ref.substring(2).split('/')
currentSchema = topLevelSchema
for (const segment of refPath) {
if (segment in currentSchema) {
currentSchema = currentSchema[segment]
} else {
throw Error(`Unable to resolve reference ${ref}`)
}
}
} else if (ref.match(/#\//g)?.length === 1) {
const [schemaUrl, relativePath] = ref.split('#/')
if (schemaUrl in schemaRefs) {
const referencedSchema = schemaRefs[schemaUrl]
const reference = { $ref: '#/'.concat(relativePath) }
const auxNextPath = []
auxNextPath.push(nextKey)
if (nextPath.length > 0) {
auxNextPath.push(...nextPath)
}
return Node._findSchema(referencedSchema, schemaRefs, auxNextPath, reference)
} else {
throw Error(`Unable to resolve reference ${ref}`)
}
} else {
throw Error(`Unable to resolve reference ${ref}`)
}
}

// We have no more path segments to resolve, return the currently found schema
// We do this here, after resolving references, in case of the leaf schema beeing a reference
if (nextKey === undefined) {
return currentSchema
}
// We have no more path segments to resolve, return the currently found schema
// We do this here, after resolving references, in case of the leaf schema beeing a reference
if (nextKey === undefined) {
return currentSchema
}

if (typeof nextKey === 'string') {
if (typeof currentSchema.properties === 'object' && currentSchema.properties !== null && nextKey in currentSchema.properties) {
currentSchema = currentSchema.properties[nextKey]
return Node._findSchema(topLevelSchema, schemaRefs, nextPath, currentSchema)
}
if (typeof currentSchema.patternProperties === 'object' && currentSchema.patternProperties !== null) {
for (const prop in currentSchema.patternProperties) {
if (nextKey.match(prop)) {
currentSchema = currentSchema.patternProperties[prop]
return Node._findSchema(topLevelSchema, schemaRefs, nextPath, currentSchema)
}
if (typeof nextKey === 'string') {
if (typeof currentSchema.properties === 'object' && currentSchema.properties !== null && nextKey in currentSchema.properties) {
currentSchema = currentSchema.properties[nextKey]
return Node._findSchema(topLevelSchema, schemaRefs, nextPath, currentSchema)
}
if (typeof currentSchema.patternProperties === 'object' && currentSchema.patternProperties !== null) {
for (const prop in currentSchema.patternProperties) {
if (nextKey.match(prop)) {
currentSchema = currentSchema.patternProperties[prop]
return Node._findSchema(topLevelSchema, schemaRefs, nextPath, currentSchema)
}
}
if (typeof currentSchema.additionalProperties === 'object') {
currentSchema = currentSchema.additionalProperties
return Node._findSchema(topLevelSchema, schemaRefs, nextPath, currentSchema)
}
continue
}
if (typeof nextKey === 'number' && typeof currentSchema.items === 'object' && currentSchema.items !== null) {
currentSchema = currentSchema.items
if (typeof currentSchema.additionalProperties === 'object') {
currentSchema = currentSchema.additionalProperties
return Node._findSchema(topLevelSchema, schemaRefs, nextPath, currentSchema)
}
return null
}
if (typeof nextKey === 'number' && typeof currentSchema.items === 'object' && currentSchema.items !== null) {
currentSchema = currentSchema.items
return Node._findSchema(topLevelSchema, schemaRefs, nextPath, currentSchema)
}

return null
}

/**
* Return the part of a JSON schema matching given path.
*
* Note that this attempts to find *a* schema matching the path, not necessarily
* the best / most appropriate. For example, oneOf vs. anyOf vs. allOf may
* result in different schemas being applied in practice.
*
* @param {Object} topLevelSchema
* @param {Object} schemaRefs
* @param {Array.<string | number>} path
* @param {Object} currentSchema
* @return {Object | null}
* @private
*/
Node._findSchema = (topLevelSchema, schemaRefs, path, currentSchema = topLevelSchema) => {
let possibleSchemas = [currentSchema]
for (const subSchemas of [currentSchema.oneOf, currentSchema.anyOf, currentSchema.allOf]) {
if (Array.isArray(subSchemas)) {
possibleSchemas = possibleSchemas.concat(subSchemas)
}
}

let fallback = null
for (const schema of possibleSchemas) {
const result = Node._findOneSchema(topLevelSchema, schemaRefs, path, schema)
// Although we don't attempt to find the best / most appropriate schema, we
// can at least attempt to find something more specific than `true`.
if (result === true) {
fallback = true
continue
} else if (result !== null) {
return result
}
}

return fallback
}

/**
* Remove nodes
* @param {Node[] | Node} nodes
Expand Down
35 changes: 35 additions & 0 deletions test/Node.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,40 @@ describe('Node', () => {
})
})

// https://json-schema.org/understanding-json-schema/reference/object#extending
it('works with extending schemas', () => {
const schema = {
properties: {
name: true,
manager: {
type: 'string',
enum: ['c', 'd']
}
},
additionalProperties: false,
allOf: [
{
properties: {
name: {
type: 'string',
enum: ['a', 'b']
}
}
}
]
}
let path = ['name']
assert.deepStrictEqual(Node._findSchema(schema, {}, path), {
type: 'string',
enum: ['a', 'b']
})
path = ['manager']
assert.deepStrictEqual(Node._findSchema(schema, {}, path), {
type: 'string',
enum: ['c', 'd']
})
})

describe('with $ref', () => {
it('should find a referenced schema', () => {
const schema = {
Expand Down Expand Up @@ -401,6 +435,7 @@ describe('Node', () => {
assert.notStrictEqual(Node._findSchema(schema, { 'definitions.json': definitions }, path), foundSchema)
})
})

describe('with pattern properties', () => {
it('should find schema', () => {
const schema = {
Expand Down

0 comments on commit 2fdbc13

Please sign in to comment.