pull/715/head
Travis Fischer 2025-06-05 18:52:11 +07:00
rodzic 2c26a75a14
commit ffd783f9e4
6 zmienionych plików z 259 dodań i 277 usunięć

Wyświetl plik

@ -19,14 +19,16 @@ export function cfValidateJsonSchemaObject<
schema,
data,
errorMessage,
coerce = true
coerce = true,
strictAdditionalProperties = true
}: {
schema: any
data: Record<string, unknown>
errorMessage?: string
coerce?: boolean
strictAdditionalProperties?: boolean
}): T {
// Special-case check for required fields to give better error messages
// Special-case check for required fields to give better error messages.
if (schema.required && Array.isArray(schema.required)) {
const missingRequiredFields: string[] = schema.required.filter(
(field: string) => (data as T)[field] === undefined
@ -40,11 +42,13 @@ export function cfValidateJsonSchemaObject<
}
}
// Special-case check for additional top-level properties, which is not
// currently handled by `@agentic/json-schema`.
// TODO: In the future, the underlying JSON schema validation should handle
// this for any sub-schema, not just the top-level one.
if (schema.properties && !schema.additionalProperties) {
// Special-case check for additional top-level fields to give better error
// messages.
if (
schema.properties &&
(schema.additionalProperties === false ||
(schema.additionalProperties === undefined && strictAdditionalProperties))
) {
const extraProperties = Object.keys(data).filter(
(key) => !schema.properties[key]
)
@ -57,7 +61,11 @@ export function cfValidateJsonSchemaObject<
}
}
const validator = new Validator({ schema, coerce })
const validator = new Validator({
schema,
coerce,
strictAdditionalProperties
})
const result = validator.validate(data)
if (result.valid) {
// console.log('validate', {

Wyświetl plik

@ -84,6 +84,10 @@ export function coerceValue({
}
break
}
if ($type === 'integer' && typeof instance === 'number') {
instance = Math.floor(instance)
}
break
case 'string':

Wyświetl plik

@ -12,15 +12,30 @@ export type Evaluated = Record<string | number, boolean>
export function validate(
instance: any,
schema: Schema | boolean,
draft: SchemaDraft = '2019-09',
lookup: Record<string, Schema | boolean> = dereference(schema),
coerce = false,
shortCircuit = true,
recursiveAnchor: Schema | null = null,
instanceLocation = '#',
schemaLocation = '#',
evaluated: Evaluated = Object.create(null)
opts: {
draft?: SchemaDraft
lookup?: Record<string, Schema | boolean>
coerce?: boolean
shortCircuit?: boolean
recursiveAnchor?: Schema | null
instanceLocation?: string
schemaLocation?: string
evaluated?: Evaluated
strictAdditionalProperties?: boolean
} = {}
): ValidationResult {
const {
draft = '2019-09',
lookup = dereference(schema),
coerce = false,
shortCircuit = true,
instanceLocation = '#',
schemaLocation = '#',
evaluated = Object.create(null),
strictAdditionalProperties = false
} = opts
let { recursiveAnchor = null } = opts
if (schema === true) {
return { valid: true, errors: [], instance }
}
@ -114,14 +129,14 @@ export function validate(
const result = validate(
instance,
recursiveAnchor === null ? schema : recursiveAnchor,
draft,
lookup,
coerce,
shortCircuit,
refSchema,
instanceLocation,
keywordLocation,
evaluated
{
...opts,
lookup,
recursiveAnchor: refSchema,
instanceLocation,
schemaLocation: keywordLocation,
evaluated
}
)
if (result.valid) {
instance = result.instance
@ -150,18 +165,14 @@ export function validate(
throw new Error(message)
}
const keywordLocation = `${schemaLocation}/$ref`
const result = validate(
instance,
refSchema,
draft,
const result = validate(instance, refSchema, {
...opts,
lookup,
coerce,
shortCircuit,
recursiveAnchor,
instanceLocation,
keywordLocation,
schemaLocation: keywordLocation,
evaluated
)
})
if (result.valid) {
instance = result.instance
} else {
@ -292,18 +303,14 @@ export function validate(
// TODO: type coercion
if ($not !== undefined) {
const keywordLocation = `${schemaLocation}/not`
const result = validate(
instance,
$not,
draft,
const result = validate(instance, $not, {
...opts,
lookup,
coerce,
shortCircuit,
recursiveAnchor,
instanceLocation,
keywordLocation /*,
evaluated*/
)
schemaLocation: keywordLocation
// evaluated
})
if (result.valid) {
errors.push({
instanceLocation,
@ -323,18 +330,14 @@ export function validate(
let anyValid = false
for (const [i, subSchema] of $anyOf.entries()) {
const subEvaluated: Evaluated = Object.create(evaluated)
const result = validate(
instance,
subSchema,
draft,
const result = validate(instance, subSchema, {
...opts,
lookup,
coerce,
shortCircuit,
$recursiveAnchor === true ? recursiveAnchor : null,
recursiveAnchor: $recursiveAnchor === true ? recursiveAnchor : null,
instanceLocation,
`${keywordLocation}/${i}`,
subEvaluated
)
schemaLocation: `${keywordLocation}/${i}`,
evaluated: subEvaluated
})
errors.push(...result.errors)
anyValid = anyValid || result.valid
if (result.valid) {
@ -360,18 +363,14 @@ export function validate(
let allValid = true
for (const [i, subSchema] of $allOf.entries()) {
const subEvaluated: Evaluated = Object.create(evaluated)
const result = validate(
instance,
subSchema,
draft,
const result = validate(instance, subSchema, {
...opts,
lookup,
coerce,
shortCircuit,
$recursiveAnchor === true ? recursiveAnchor : null,
recursiveAnchor: $recursiveAnchor === true ? recursiveAnchor : null,
instanceLocation,
`${keywordLocation}/${i}`,
subEvaluated
)
schemaLocation: `${keywordLocation}/${i}`,
evaluated: subEvaluated
})
errors.push(...result.errors)
allValid = allValid && result.valid
if (result.valid) {
@ -396,18 +395,14 @@ export function validate(
const errorsLength = errors.length
const matches = $oneOf.filter((subSchema, i) => {
const subEvaluated: Evaluated = Object.create(evaluated)
const result = validate(
instance,
subSchema,
draft,
const result = validate(instance, subSchema, {
...opts,
lookup,
coerce,
shortCircuit,
$recursiveAnchor === true ? recursiveAnchor : null,
recursiveAnchor: $recursiveAnchor === true ? recursiveAnchor : null,
instanceLocation,
`${keywordLocation}/${i}`,
subEvaluated
)
schemaLocation: `${keywordLocation}/${i}`,
evaluated: subEvaluated
})
errors.push(...result.errors)
if (result.valid) {
subEvaluateds.push(subEvaluated)
@ -432,32 +427,24 @@ export function validate(
if ($if !== undefined) {
const keywordLocation = `${schemaLocation}/if`
const conditionResult = validate(
instance,
$if,
draft,
const conditionResult = validate(instance, $if, {
...opts,
lookup,
coerce,
shortCircuit,
recursiveAnchor,
instanceLocation,
keywordLocation,
schemaLocation: keywordLocation,
evaluated
).valid
}).valid
if (conditionResult) {
if ($then !== undefined) {
const thenResult = validate(
instance,
$then,
draft,
const thenResult = validate(instance, $then, {
...opts,
lookup,
coerce,
shortCircuit,
recursiveAnchor,
instanceLocation,
`${schemaLocation}/then`,
schemaLocation: `${schemaLocation}/then`,
evaluated
)
})
if (thenResult.valid) {
instance = thenResult.instance
} else {
@ -473,18 +460,14 @@ export function validate(
}
}
} else if ($else !== undefined) {
const elseResult = validate(
instance,
$else,
draft,
const elseResult = validate(instance, $else, {
...opts,
lookup,
coerce,
shortCircuit,
recursiveAnchor,
instanceLocation,
`${schemaLocation}/else`,
schemaLocation: `${schemaLocation}/else`,
evaluated
)
})
if (elseResult.valid) {
instance = elseResult.instance
} else {
@ -539,17 +522,13 @@ export function validate(
const keywordLocation = `${schemaLocation}/propertyNames`
for (const key in instance) {
const subInstancePointer = `${instanceLocation}/${encodePointer(key)}`
const result = validate(
key,
$propertyNames,
draft,
const result = validate(key, $propertyNames, {
...opts,
lookup,
coerce,
shortCircuit,
recursiveAnchor,
subInstancePointer,
keywordLocation
)
instanceLocation: subInstancePointer,
schemaLocation: keywordLocation
})
if (!result.valid) {
errors.push(
{
@ -587,18 +566,14 @@ export function validate(
for (const key in $dependentSchemas) {
const keywordLocation = `${schemaLocation}/dependentSchemas`
if (key in instance) {
const result = validate(
instance,
$dependentSchemas[key]!,
draft,
const result = validate(instance, $dependentSchemas[key]!, {
...opts,
lookup,
coerce,
shortCircuit,
recursiveAnchor,
instanceLocation,
`${keywordLocation}/${encodePointer(key)}`,
schemaLocation: `${keywordLocation}/${encodePointer(key)}`,
evaluated
)
})
if (!result.valid) {
errors.push(
{
@ -631,17 +606,13 @@ export function validate(
}
}
} else {
const result = validate(
instance,
propsOrSchema,
draft,
const result = validate(instance, propsOrSchema, {
...opts,
lookup,
coerce,
shortCircuit,
recursiveAnchor,
instanceLocation,
`${keywordLocation}/${encodePointer(key)}`
)
schemaLocation: `${keywordLocation}/${encodePointer(key)}`
})
if (!result.valid) {
errors.push(
{
@ -669,17 +640,13 @@ export function validate(
continue
}
const subInstancePointer = `${instanceLocation}/${encodePointer(key)}`
const result = validate(
instance[key],
$properties[key]!,
draft,
const result = validate(instance[key], $properties[key]!, {
...opts,
lookup,
coerce,
shortCircuit,
recursiveAnchor,
subInstancePointer,
`${keywordLocation}/${encodePointer(key)}`
)
instanceLocation: subInstancePointer,
schemaLocation: `${keywordLocation}/${encodePointer(key)}`
})
if (result.valid) {
evaluated[key] = thisEvaluated[key] = true
instance[key] = result.instance
@ -709,17 +676,13 @@ export function validate(
continue
}
const subInstancePointer = `${instanceLocation}/${encodePointer(key)}`
const result = validate(
instance[key],
subSchema!,
draft,
const result = validate(instance[key], subSchema!, {
...opts,
lookup,
coerce,
shortCircuit,
recursiveAnchor,
subInstancePointer,
`${keywordLocation}/${encodePointer(pattern)}`
)
instanceLocation: subInstancePointer,
schemaLocation: `${keywordLocation}/${encodePointer(pattern)}`
})
if (result.valid) {
evaluated[key] = thisEvaluated[key] = true
instance[key] = result.instance
@ -739,7 +702,10 @@ export function validate(
}
}
if (!stop && $additionalProperties !== undefined) {
if (
!stop &&
($additionalProperties !== undefined || strictAdditionalProperties)
) {
const keywordLocation = `${schemaLocation}/additionalProperties`
for (const key in instance) {
if (thisEvaluated[key]) {
@ -748,14 +714,14 @@ export function validate(
const subInstancePointer = `${instanceLocation}/${encodePointer(key)}`
const result = validate(
instance[key],
$additionalProperties,
draft,
lookup,
coerce,
shortCircuit,
recursiveAnchor,
subInstancePointer,
keywordLocation
$additionalProperties ?? !strictAdditionalProperties,
{
...opts,
lookup,
recursiveAnchor,
instanceLocation: subInstancePointer,
schemaLocation: keywordLocation
}
)
if (result.valid) {
evaluated[key] = true
@ -778,17 +744,13 @@ export function validate(
for (const key in instance) {
if (!evaluated[key]) {
const subInstancePointer = `${instanceLocation}/${encodePointer(key)}`
const result = validate(
instance[key],
$unevaluatedProperties,
draft,
const result = validate(instance[key], $unevaluatedProperties, {
...opts,
lookup,
coerce,
shortCircuit,
recursiveAnchor,
subInstancePointer,
keywordLocation
)
instanceLocation: subInstancePointer,
schemaLocation: keywordLocation
})
if (result.valid) {
evaluated[key] = true
instance[key] = result.instance
@ -833,17 +795,13 @@ export function validate(
const keywordLocation = `${schemaLocation}/prefixItems`
const length2 = Math.min($prefixItems.length, length)
for (; i < length2; i++) {
const result = validate(
instance[i],
$prefixItems[i]!,
draft,
const result = validate(instance[i], $prefixItems[i]!, {
...opts,
lookup,
coerce,
shortCircuit,
recursiveAnchor,
`${instanceLocation}/${i}`,
`${keywordLocation}/${i}`
)
instanceLocation: `${instanceLocation}/${i}`,
schemaLocation: `${keywordLocation}/${i}`
})
evaluated[i] = true
if (!result.valid) {
stop = shortCircuit
@ -866,17 +824,13 @@ export function validate(
if (Array.isArray($items)) {
const length2 = Math.min($items.length, length)
for (; i < length2; i++) {
const result = validate(
instance[i],
$items[i]!,
draft,
const result = validate(instance[i], $items[i]!, {
...opts,
lookup,
coerce,
shortCircuit,
recursiveAnchor,
`${instanceLocation}/${i}`,
`${keywordLocation}/${i}`
)
instanceLocation: `${instanceLocation}/${i}`,
schemaLocation: `${keywordLocation}/${i}`
})
evaluated[i] = true
if (result.valid) {
instance[i] = result.instance
@ -896,17 +850,13 @@ export function validate(
}
} else {
for (; i < length; i++) {
const result = validate(
instance[i],
$items,
draft,
const result = validate(instance[i], $items, {
...opts,
lookup,
coerce,
shortCircuit,
recursiveAnchor,
`${instanceLocation}/${i}`,
keywordLocation
)
instanceLocation: `${instanceLocation}/${i}`,
schemaLocation: keywordLocation
})
evaluated[i] = true
if (result.valid) {
instance[i] = result.instance
@ -929,17 +879,13 @@ export function validate(
if (!stop && $additionalItems !== undefined) {
const keywordLocation = `${schemaLocation}/additionalItems`
for (; i < length; i++) {
const result = validate(
instance[i],
$additionalItems,
draft,
const result = validate(instance[i], $additionalItems, {
...opts,
lookup,
coerce,
shortCircuit,
recursiveAnchor,
`${instanceLocation}/${i}`,
keywordLocation
)
instanceLocation: `${instanceLocation}/${i}`,
schemaLocation: keywordLocation
})
evaluated[i] = true
if (result.valid) {
instance[i] = result.instance
@ -979,17 +925,13 @@ export function validate(
const errorsLength = errors.length
let contained = 0
for (let j = 0; j < length; j++) {
const result = validate(
instance[j],
$contains,
draft,
const result = validate(instance[j], $contains, {
...opts,
lookup,
coerce,
shortCircuit,
recursiveAnchor,
`${instanceLocation}/${j}`,
keywordLocation
)
instanceLocation: `${instanceLocation}/${j}`,
schemaLocation: keywordLocation
})
if (result.valid) {
evaluated[j] = true
contained++
@ -1037,17 +979,13 @@ export function validate(
if (evaluated[i]) {
continue
}
const result = validate(
instance[i],
$unevaluatedItems,
draft,
const result = validate(instance[i], $unevaluatedItems, {
...opts,
lookup,
coerce,
shortCircuit,
recursiveAnchor,
`${instanceLocation}/${i}`,
keywordLocation
)
instanceLocation: `${instanceLocation}/${i}`,
schemaLocation: keywordLocation
})
evaluated[i] = true
if (result.valid) {
instance[i] = result.instance

Wyświetl plik

@ -3,39 +3,42 @@ import { dereference } from './dereference'
import { validate } from './validate'
export class Validator {
private readonly lookup: ReturnType<typeof dereference>
private readonly schema: Schema | boolean
private readonly draft: SchemaDraft
private readonly shortCircuit: boolean
private readonly coerce: boolean
protected readonly lookup: ReturnType<typeof dereference>
protected readonly schema: Schema | boolean
protected readonly draft: SchemaDraft
protected readonly shortCircuit: boolean
protected readonly coerce: boolean
protected readonly strictAdditionalProperties: boolean
constructor({
schema,
draft = '2019-09',
shortCircuit = true,
coerce = false
coerce = false,
strictAdditionalProperties = false
}: {
schema: Schema | boolean
draft?: SchemaDraft
shortCircuit?: boolean
coerce?: boolean
strictAdditionalProperties?: boolean
}) {
this.schema = schema
this.draft = draft
this.shortCircuit = shortCircuit
this.coerce = coerce
this.strictAdditionalProperties = strictAdditionalProperties
this.lookup = dereference(schema)
}
public validate(instance: any): ValidationResult {
return validate(
structuredClone(instance),
this.schema,
this.draft,
this.lookup,
this.coerce,
this.shortCircuit
)
return validate(structuredClone(instance), this.schema, {
draft: this.draft,
lookup: this.lookup,
coerce: this.coerce,
shortCircuit: this.shortCircuit,
strictAdditionalProperties: this.strictAdditionalProperties
})
}
public addSchema(schema: Schema, id?: string): void {

Wyświetl plik

@ -4,114 +4,143 @@ import { validate } from '../src/index'
describe('json-schema coercion', () => {
it('string => number coercion', () => {
const result = validate('7', { type: 'number' }, '2019-09', undefined, true)
const result = validate('7', { type: 'number' }, { coerce: true })
expect(result.valid).to.equal(true)
expect(result.instance).to.equal(7)
})
it('boolean => number coercion', () => {
const result = validate(
true,
{ type: 'number' },
'2019-09',
undefined,
true
)
const result = validate(true, { type: 'number' }, { coerce: true })
expect(result.valid).to.equal(true)
expect(result.instance).to.equal(1)
})
it('null => number coercion', () => {
const result = validate(
null,
{ type: 'number' },
'2019-09',
undefined,
true
)
const result = validate(null, { type: 'number' }, { coerce: true })
expect(result.valid).to.equal(true)
expect(result.instance).to.equal(0)
})
it('array => number coercion', () => {
const result = validate([1], { type: 'number' }, '2019-09', undefined, true)
const result = validate([1], { type: 'number' }, { coerce: true })
expect(result.valid).to.equal(true)
expect(result.instance).to.equal(1)
})
it('boolean => string coercion', () => {
const result = validate(
true,
{ type: 'string' },
'2019-09',
undefined,
true
)
const result = validate(true, { type: 'string' }, { coerce: true })
expect(result.valid).to.equal(true)
expect(result.instance).to.equal('true')
})
it('number => string coercion', () => {
const result = validate(
72.3,
{ type: 'string' },
'2019-09',
undefined,
true
)
const result = validate(72.3, { type: 'string' }, { coerce: true })
expect(result.valid).to.equal(true)
expect(result.instance).to.equal('72.3')
})
it('null => string coercion', () => {
const result = validate(
null,
{ type: 'string' },
'2019-09',
undefined,
true
)
const result = validate(null, { type: 'string' }, { coerce: true })
expect(result.valid).to.equal(true)
expect(result.instance).to.equal('')
})
it('array => string coercion', () => {
const result = validate(
['nala'],
{ type: 'string' },
'2019-09',
undefined,
true
)
const result = validate(['nala'], { type: 'string' }, { coerce: true })
expect(result.valid).to.equal(true)
expect(result.instance).to.equal('nala')
})
it('string => boolean coercion', () => {
const result = validate(
'true',
{ type: 'boolean' },
'2019-09',
undefined,
true
)
const result = validate('true', { type: 'boolean' }, { coerce: true })
expect(result.valid).to.equal(true)
expect(result.instance).to.equal(true)
})
it('string => null coercion', () => {
const result = validate('', { type: 'null' }, '2019-09', undefined, true)
const result = validate('', { type: 'null' }, { coerce: true })
expect(result.valid).to.equal(true)
expect(result.instance).to.equal(null)
})
it('object property coercion', () => {
const result = validate(
{
name: null,
cool: 'true',
cool2: 0,
number: '5.12',
integer: '5.12'
},
{
type: 'object',
properties: {
name: { type: 'string' },
cool: { type: 'boolean' },
cool2: { type: 'boolean' },
number: { type: 'number' },
integer: { type: 'integer' }
}
},
{ coerce: true }
)
expect(result.valid).to.equal(true)
expect(result.instance).to.deep.equal({
name: '',
cool: true,
cool2: false,
number: 5.12,
integer: 5
})
})
it('strictAdditionalProperties false', () => {
const result = validate(
{
name: 'nala',
extra: true
},
{
type: 'object',
properties: {
name: { type: 'string' }
}
}
)
expect(result.valid).to.equal(true)
expect(result.instance).to.deep.equal({
name: 'nala',
extra: true
})
})
it('strictAdditionalProperties true', () => {
const result = validate(
{
name: 'nala',
extra: true
},
{
type: 'object',
properties: {
name: { type: 'string' }
}
},
{ strictAdditionalProperties: true }
)
expect(result.valid).to.equal(false)
})
})

Wyświetl plik

@ -60,7 +60,7 @@ describe('json-schema', () => {
}
let result: ValidationResult | undefined
try {
result = validate(data, schema, draft, lookup)
result = validate(data, schema, { draft, lookup })
} catch {}
if (result?.valid !== valid) {
failures[name] = failures[name] ?? {}