Skip to content

Commit

Permalink
feat: don't ensure full top-level validation in refs
Browse files Browse the repository at this point in the history
  • Loading branch information
ChALkeR committed Aug 8, 2023
1 parent 5f0550a commit e629094
Show file tree
Hide file tree
Showing 2 changed files with 207 additions and 18 deletions.
42 changes: 24 additions & 18 deletions src/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -1184,15 +1184,17 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => {
fail('failed to resolve $ref:', $ref)
if (lintOnly) return null // failures are just collected in linter mode and don't throw, this makes a ref noop
}
if (sub.type) {
// This could be done better, but for now we check only the direct type in the $ref
const type = Array.isArray(sub.type) ? sub.type : [sub.type]
evaluateDelta({ type })
if (requireValidation) {
// If validation is required, then $ref is guranteed to validate all items and properties
if (type.includes('array')) evaluateDelta({ items: Infinity })
if (type.includes('object')) evaluateDelta({ properties: [true] })
}
// evaluation prior to ref application is needed for recursion cases
// This could be done better, but for now we check only the direct rules in the $ref
if (sub.type) evaluateDelta({ type: Array.isArray(sub.type) ? sub.type : [sub.type] })
for (const t of ['additionalProperties', 'unevaluatedProperties']) {
if (functions.hasOwn(sub, t) && isSchemaish(sub[t])) evaluateDelta({ properties: [true] })
}
for (const t of ['additionalItems', 'unevaluatedItems', 'items']) {
if (functions.hasOwn(sub, t) && isSchemaish(sub[t])) evaluateDelta({ items: Infinity })
}
for (const t of ['pattern', 'format']) {
if (typeof sub[t] === 'string') evaluateDelta({ fullstring: true })
}
return applyRef(compileSub(sub, subRoot, path), { path: ['$ref'] })
})
Expand Down Expand Up @@ -1297,18 +1299,22 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => {
} else if (!schemaPath.includes('not')) {
// 'not' does not mark anything as evaluated (unlike even if/then/else), so it's safe to exclude from these
// checks, as we are sure that everything will be checked without it. It can be viewed as a pure add-on.
if (!stat.type) enforceValidation('type')
if (typeApplicable('array') && stat.items !== Infinity)
enforceValidation(node.items ? 'additionalItems or unevaluatedItems' : 'items rule')
if (typeApplicable('object') && !stat.properties.includes(true))
enforceValidation('additionalProperties or unevaluatedProperties')
const isRefTop = schema !== root && node === schema // We are at the top-level of an opaque ref inside the schema object
if (!isRefTop) {
if (!stat.type) enforceValidation('type')
// This can't be true for top-level schemas, only references with #/...
if (typeApplicable('array') && stat.items !== Infinity)
enforceValidation(node.items ? 'additionalItems or unevaluatedItems' : 'items rule')
if (typeApplicable('object') && !stat.properties.includes(true))
enforceValidation('additionalProperties or unevaluatedProperties')
if (!stat.fullstring && requireStringValidation) {
const stringWarning = 'pattern, format or contentSchema should be specified for strings'
fail(`[requireStringValidation] ${stringWarning}, use pattern: ^[\\s\\S]*$ to opt-out`)
}
}
if (typeof node.propertyNames !== 'object')
for (const sub of ['additionalProperties', 'unevaluatedProperties'])
if (node[sub]) enforceValidation(`wild-card ${sub}`, 'requires propertyNames')
if (!stat.fullstring && requireStringValidation) {
const stringWarning = 'pattern, format or contentSchema should be specified for strings'
fail(`[requireStringValidation] ${stringWarning}, use pattern: ^[\\s\\S]*$ to opt-out`)
}
}
if (node.properties && !node.required) enforceValidation('if properties is used, required')
enforce(unused.size === 0 || allowUnusedKeywords, 'Unprocessed keywords:', [...unused])
Expand Down
183 changes: 183 additions & 0 deletions test/regressions/strong-refs.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,186 @@ tape('cyclic ref passes if fully covers the object', (t) => {

t.end()
})

tape('partially validating local ref can be used (objects)', (t) => {
const propTwoBare = { properties: { two: { const: 2 } }, required: ['two'] }
const propTwoFull = { ...propTwoBare, type: 'object' }
const compile = schema => validator(
{
$schema: 'https://json-schema.org/draft/2020-12/schema',
...schema,
},
{ mode: 'strong' }
)

t.throws(() => compile(propTwoBare), /\[requireValidation\] type should be specified/)
t.throws(() => compile(propTwoFull), /\[requireValidation\].*Properties should be specified/)

t.throws(() => {
compile(
{
$ref: '#/$defs/propTwo',
$defs: {
propTwo: propTwoBare,
},
// type: 'object',
unevaluatedProperties: false,
},
{ mode: 'strong' }
)
})
t.throws(() => {
compile(
{
$ref: '#/$defs/propTwo',
$defs: {
propTwo: propTwoBare,
},
type: 'object',
// unevaluatedProperties: false,
},
{ mode: 'strong' }
)
})
t.doesNotThrow(() => {
const validate = compile(
{
$ref: '#/$defs/propTwo',
$defs: {
propTwo: propTwoBare,
},
type: 'object',
unevaluatedProperties: false,
}
)
t.notOk(validate({}), '{}')
t.ok(validate({ two: 2 }), '{two:2}')
t.notOk(validate({ three: 2 }), '{three:2}')
t.notOk(validate({ two: 3 }), '{two:3}')
t.notOk(validate({ two: 2, three: 3 }), '{two:2, three:3}')
t.notOk(validate({ two: 2, one: 1 }), '{two:2, one:1}')
t.notOk(validate({ one: 1 }), '{one:1}')
})
t.doesNotThrow(() => {
const validate = compile(
{
$ref: '#/$defs/propTwo',
$defs: {
propTwo: propTwoFull,
},
unevaluatedProperties: false,
}
)
t.notOk(validate({}), '{}')
t.ok(validate({ two: 2 }), '{two:2}')
t.notOk(validate({ three: 2 }), '{three:2}')
t.notOk(validate({ two: 3 }), '{two:3}')
t.notOk(validate({ two: 2, three: 3 }), '{two:2, three:3}')
t.notOk(validate({ two: 2, one: 1 }), '{two:2, one:1}')
t.notOk(validate({ one: 1 }), '{one:1}')
})
t.doesNotThrow(() => {
const validate = compile(
{
$ref: '#/$defs/propTwo',
$defs: {
propTwo: propTwoBare,
},
required: [],
properties: {
one: { const: 1 },
},
type: 'object',
unevaluatedProperties: false,
}
)
t.notOk(validate({}), '{}')
t.ok(validate({ two: 2 }), '{two:2}')
t.notOk(validate({ three: 2 }), '{three:2}')
t.notOk(validate({ two: 3 }), '{two:3}')
t.notOk(validate({ two: 2, three: 3 }), '{two:2, three:3}')
t.ok(validate({ two: 2, one: 1 }), '{two:2, one:1}')
t.notOk(validate({ one: 1 }), '{one:1}')
})
t.doesNotThrow(() => {
const validate = compile(
{
$ref: '#/$defs/propTwo',
$defs: {
propTwo: propTwoFull,
},
required: [],
properties: {
one: { const: 1 },
},
unevaluatedProperties: false,
}
)
t.notOk(validate({}), '{}')
t.ok(validate({ two: 2 }), '{two:2}')
t.notOk(validate({ three: 2 }), '{three:2}')
t.notOk(validate({ two: 3 }), '{two:3}')
t.notOk(validate({ two: 2, three: 3 }), '{two:2, three:3}')
t.ok(validate({ two: 2, one: 1 }), '{two:2, one:1}')
t.notOk(validate({ one: 1 }), '{one:1}')
})

t.end()
})


tape('partially validating local ref can be used (strings)', (t) => {
const stringDateBare = { format: 'date' }
const stringDateFull = { ...stringDateBare, type: 'string' }
const compile = schema => validator(
{
$schema: 'https://json-schema.org/draft/2020-12/schema',
...schema,
},
{ mode: 'strong' }
)

t.throws(() => compile(stringDateBare), /\[requireValidation\] type should be specified/)
t.doesNotThrow(() => compile(stringDateFull))

t.throws(() => {
compile(
{
$ref: '#/$defs/stringDate',
$defs: {
stringDate: stringDateBare,
},
// type: 'string',
},
{ mode: 'strong' }
)
})
t.doesNotThrow(() => {
const validate = compile(
{
$ref: '#/$defs/stringDate',
$defs: {
stringDate: stringDateBare,
},
type: 'string',
}
)
t.notOk(validate(''), '""')
t.notOk(validate('abc'), '"abc"')
t.ok(validate('2000-01-01'), '"2000-01-01"')
})
t.doesNotThrow(() => {
const validate = compile(
{
$ref: '#/$defs/stringDate',
$defs: {
stringDate: stringDateFull,
},
}
)
t.notOk(validate(''), '""')
t.notOk(validate('abc'), '"abc"')
t.ok(validate('2000-01-01'), '"2000-01-01"')
})
t.end()
})

0 comments on commit e629094

Please sign in to comment.