import { runInNewContext } from 'node:vm' import { describe, expect, it } from 'vitest' import { assertObjectJsonSchema, assertSupportedJsonSchema, JsonSchemaError, validateJsonSchemaValue, type JsonSchemaNode, type ObjectJsonSchema, } from '../src/index.ts' function asserted(schema: unknown): JsonSchemaNode { assertSupportedJsonSchema(schema) return schema } function assertedObject(schema: unknown): ObjectJsonSchema { assertObjectJsonSchema(schema) return schema } function violationsOf(schema: unknown, objectRoot = false): string[] { try { if (objectRoot) assertObjectJsonSchema(schema) else assertSupportedJsonSchema(schema) } catch (error: unknown) { if (error instanceof JsonSchemaError) return error.violations throw error } throw new Error('expected schema rejection') } function recordWithForgedIntrinsicPrototype( own: Record, inherited: Record = {}, revoked = false, ): Record { const prototype = Object.assign(Object.create(null) as Record, inherited) const ForgedObject = function ForgedObject(): void {} Object.defineProperty(ForgedObject, 'name', { value: 'Object' }) ForgedObject.prototype = prototype const constructor = revoked ? Proxy.revocable(ForgedObject, {}) : undefined if (constructor !== undefined) constructor.revoke() Object.defineProperty(prototype, 'constructor', { value: constructor?.proxy ?? ForgedObject }) return Object.assign(Object.create(prototype) as Record, own) } describe('the enforced raw JSON Schema subset', () => { it('accepts every JSON root and every supported node', () => { for (const schema of [ { type: 'string' }, { type: 'number' }, { type: 'integer' }, { type: 'boolean' }, { type: 'null' }, { type: 'array', items: { type: 'string' } }, { type: 'object', properties: { nested: { type: 'object', properties: {}, additionalProperties: false }, free: {}, }, required: ['nested'], additionalProperties: true, }, { oneOf: [{ type: 'string' }, { type: 'number' }] }, { description: 'any JSON', title: 'JSON', default: null, examples: [1, 'x'] }, ]) { expect(() => { assertSupportedJsonSchema(schema) }, JSON.stringify(schema)).not.toThrow() } }) it('retains an object-root guard only at consumers that need it', () => { expect(assertedObject({ type: 'object' }).type).toBe('object') for (const schema of [{}, { type: 'string' }, { type: 'array' }, { oneOf: [{ type: 'string' }, { type: 'null' }] }]) { expect(violationsOf(schema, true)).toEqual(['schema.type must be "object" (structured output is object-rooted)']) } }) it('rejects non-schema nodes, unknown types, and type arrays', () => { expect(violationsOf(null)).toEqual(['schema must be a schema object']) expect(violationsOf([])).toEqual(['schema must be a schema object']) expect(violationsOf('no')).toEqual(['schema must be a schema object']) expect(violationsOf({ type: 'tuple' })[0]).toMatch(/type must be one of/) expect(violationsOf({ type: ['string', 'null'] })) .toEqual(['schema.type must be a single type string (type arrays are not supported)']) }) it('enforces oneOf vocabulary and its minimum branch count', () => { expect(violationsOf({ oneOf: [] })).toEqual(['schema.oneOf must be an array of at least two schemas']) expect(violationsOf({ oneOf: [{}] })).toEqual(['schema.oneOf must be an array of at least two schemas']) expect(violationsOf({ oneOf: 'x' })).toEqual(['schema.oneOf must be an array of at least two schemas']) expect(violationsOf({ type: 'string', oneOf: [{}, {}] })) .toEqual(['schema cannot declare both type and oneOf']) expect(violationsOf({ oneOf: [{ type: 'string' }, { type: 'number' }], items: {} })) .toEqual(['schema.items is not supported beside oneOf']) expect(violationsOf({ oneOf: [{ type: 'string' }, { type: 'weird' }] })[0]) .toContain('schema.oneOf[1].type') const sparse = new Array(2) sparse[0] = { type: 'string' } expect(violationsOf({ oneOf: sparse })) .toEqual(['schema.oneOf must be an array of at least two schemas']) const compensatedSparse = new Array(2) compensatedSparse[0] = { type: 'string' } Object.defineProperty(compensatedSparse, 'extra', { value: true }) expect(violationsOf({ oneOf: compensatedSparse })) .toEqual(['schema.oneOf must be an array of at least two schemas']) class ExoticBranches extends Array {} expect(violationsOf({ oneOf: new ExoticBranches({ type: 'string' }, { type: 'null' }) })) .toEqual(['schema.oneOf must be an array of at least two schemas']) const explosiveArray = new Proxy([{ type: 'string' }, { type: 'null' }], { getPrototypeOf() { throw new Error('prototype trap') }, }) expect(violationsOf({ oneOf: explosiveArray })) .toEqual(['schema.oneOf must be an array of at least two schemas']) }) it('rejects unknown and misplaced keywords without accepted-then-ignored behavior', () => { for (const keyword of ['anyOf', 'allOf', 'not', 'pattern', 'minimum', 'maxLength', '$ref']) { expect(violationsOf({ type: 'object', [keyword]: [] })[0]).toContain(`schema.${keyword} is not a supported keyword`) } expect(violationsOf({ type: 'object', items: {} })) .toEqual(['schema.items is not supported on type "object"']) expect(violationsOf({ type: 'array', properties: {} })) .toEqual(['schema.properties is not supported on type "array"']) expect(violationsOf({ type: 'object', enum: ['x'] })) .toEqual(['schema.enum is not supported on type "object"']) expect(violationsOf({ type: 'array', const: null })) .toEqual(['schema.const is not supported on type "array"']) expect(violationsOf({ properties: {}, required: [], additionalProperties: true, items: {}, enum: [], const: null })) .toEqual([ 'schema.properties requires type or oneOf', 'schema.required requires type or oneOf', 'schema.additionalProperties requires type or oneOf', 'schema.items requires type or oneOf', 'schema.enum requires type or oneOf', 'schema.const requires type or oneOf', ]) }) it('reports every independent schema violation', () => { expect(violationsOf({ type: 'object', pattern: 'x', properties: { a: { type: 'weird' }, b: { type: 'string', minimum: 1 } }, })).toHaveLength(3) }) it('validates object properties, required names, and openness', () => { expect(violationsOf({ type: 'object', properties: [] })) .toEqual(['schema.properties must be an object of schemas']) expect(violationsOf({ type: 'object', properties: { a: 'x' } })) .toEqual(['schema.properties.a must be a schema object']) expect(violationsOf({ type: 'object', required: 'a' })) .toEqual(['schema.required must be an array of strings']) expect(violationsOf({ type: 'object', required: [1] })) .toEqual(['schema.required must be an array of strings']) expect(violationsOf({ type: 'object', properties: {}, required: ['missing'] })) .toEqual(['schema.required names "missing" which is not in properties']) expect(violationsOf({ type: 'object', additionalProperties: 'yes' })) .toEqual(['schema.additionalProperties must be a boolean']) expect(violationsOf({ type: 'object', properties: undefined })) .toEqual(['schema.properties must be an object of schemas']) expect(violationsOf({ type: 'object', properties: undefined, required: ['missing'] })) .toEqual([ 'schema.properties must be an object of schemas', 'schema.required names "missing" which is not in properties', ]) const sparseRequired = new Array(1) expect(violationsOf({ type: 'object', required: sparseRequired })) .toEqual(['schema.required must be an array of strings']) }) it('requires type-correct scalar enum and const values', () => { for (const schema of [ { type: 'string', enum: ['a'], const: 'a' }, { type: 'number', enum: [1.5], const: 1.5 }, { type: 'integer', enum: [1], const: 1 }, { type: 'boolean', enum: [true], const: true }, { type: 'null', enum: [null], const: null }, ]) { expect(() => { assertSupportedJsonSchema(schema) }, JSON.stringify(schema)).not.toThrow() } expect(violationsOf({ type: 'string', enum: [] })) .toEqual(['schema.enum must be a non-empty array of string values']) expect(violationsOf({ type: 'number', enum: ['1'] })) .toEqual(['schema.enum must be a non-empty array of number values']) expect(violationsOf({ type: 'integer', enum: [1.5] })) .toEqual(['schema.enum must be a non-empty array of integer values']) expect(violationsOf({ type: 'number', enum: [Number.NaN] })) .toEqual(['schema.enum must be a non-empty array of number values']) expect(violationsOf({ type: 'number', const: -0 })) .toEqual(['schema.const must be a number value']) expect(violationsOf({ type: 'boolean', const: 1 })) .toEqual(['schema.const must be a boolean value']) expect(violationsOf({ type: 'string', enum: undefined })) .toEqual(['schema.enum must be a non-empty array of string values']) expect(violationsOf({ type: 'string', enum: ['a'], const: 'b' })) .toEqual(['schema.const must be one of schema.enum when both are declared']) const sparseEnum = new Array(1) expect(violationsOf({ type: 'string', enum: sparseEnum })) .toEqual(['schema.enum must be a non-empty array of string values']) }) it('validates annotation types and lossless JSON payloads', () => { expect(violationsOf({ description: 1 })).toEqual(['schema.description must be a string']) expect(violationsOf({ title: 1 })).toEqual(['schema.title must be a string']) for (const [key, value] of [ ['default', undefined], ['examples', [undefined]], ['default', Number.POSITIVE_INFINITY], ['examples', new Date(0)], ] as const) { expect(violationsOf({ [key]: value })).toEqual([`schema.${key} annotation must be lossless JSON data`]) } const cyclic: Record = {} cyclic.self = cyclic expect(violationsOf({ default: cyclic })) .toEqual(['schema.default annotation must be lossless JSON data']) const explosive = new Proxy({}, { ownKeys() { throw new Error('annotation trap') }, }) expect(violationsOf({ examples: explosive })) .toEqual(['schema.examples annotation must be lossless JSON data']) expect(violationsOf({ default: Object.defineProperty({}, 'hidden', { value: true }) })) .toEqual(['schema.default annotation must be lossless JSON data']) expect(violationsOf({ default: { [Symbol('hidden')]: true } })) .toEqual(['schema.default annotation must be lossless JSON data']) }) it('accepts lossless annotation containers from another JavaScript realm', () => { const schema = runInNewContext(`({ type: 'object', properties: { value: { type: 'string', enum: ['x'] } }, required: ['value'], default: { x: 1 }, examples: [[{ ok: true }]], })`) as unknown expect(() => { assertSupportedJsonSchema(schema) }).not.toThrow() }) it('rejects cyclic/exotic schema structure but permits sibling reuse', () => { const cyclic: Record = { type: 'object' } cyclic.properties = { self: cyclic } expect(violationsOf(cyclic)).toEqual(['schema.properties.self is circular']) const leaf = { type: 'string' } expect(() => { assertSupportedJsonSchema({ type: 'object', properties: { a: leaf, b: leaf } }) }).not.toThrow() expect(violationsOf({ type: 'object', properties: new Map() })) .toEqual(['schema.properties must be an object of schemas']) expect(violationsOf({ type: 'object', properties: { at: new Date(0) } })) .toEqual(['schema.properties.at must be a schema object']) const forgedSchema = recordWithForgedIntrinsicPrototype( { type: 'object' }, { oneOf: [{ type: 'string' }, { type: 'null' }] }, ) expect(violationsOf(forgedSchema)).toEqual(['schema must be a schema object']) expect(violationsOf(forgedSchema, true)).toEqual(['schema must be a schema object']) expect(violationsOf(recordWithForgedIntrinsicPrototype({ type: 'string' }, {}, true))) .toEqual(['schema must be a schema object']) const prototypeWithoutConstructor = Object.create(null) as object expect(violationsOf(Object.create(prototypeWithoutConstructor) as unknown)) .toEqual(['schema must be a schema object']) expect(violationsOf(Object.defineProperty({ type: 'string' }, 'hidden', { value: true }))) .toEqual(['schema must be a schema object']) expect(violationsOf({ type: 'string', [Symbol('hidden')]: true })) .toEqual(['schema must be a schema object']) expect(violationsOf(new Proxy({}, { getPrototypeOf() { throw new Error('prototype trap') }, }))).toEqual(['schema must be a schema object']) expect(violationsOf(new Proxy({}, { ownKeys() { throw new Error('keys trap') }, }))).toEqual(['schema must be a schema object']) }) it('asserts deeply nested raw unions without using the JavaScript call stack', () => { const depth = 5_000 let schema: JsonSchemaNode = { type: 'string' } for (let index = 0; index < depth; index++) schema = { oneOf: [schema, { type: 'null' }] } expect(() => { assertSupportedJsonSchema(schema) }).not.toThrow() }) it('uses own-property semantics for required declarations', () => { expect(violationsOf({ type: 'object', properties: {}, required: ['toString'] })) .toEqual(['schema.required names "toString" which is not in properties']) }) }) describe('validateJsonSchemaValue', () => { it('validates scalar, array, object, and null roots', () => { expect(validateJsonSchemaValue(asserted({ type: 'string' }), 'x')).toEqual([]) expect(validateJsonSchemaValue(asserted({ type: 'number' }), 1.5)).toEqual([]) expect(validateJsonSchemaValue(asserted({ type: 'integer' }), 2)).toEqual([]) expect(validateJsonSchemaValue(asserted({ type: 'boolean' }), true)).toEqual([]) expect(validateJsonSchemaValue(asserted({ type: 'null' }), null)).toEqual([]) expect(validateJsonSchemaValue(asserted({ type: 'array', items: { type: 'string' } }), ['x'])).toEqual([]) expect(validateJsonSchemaValue(asserted({ type: 'object' }), { x: 1 })).toEqual([]) }) it('rejects wrong scalar types and lossy numbers', () => { expect(validateJsonSchemaValue(asserted({ type: 'string' }), 1)).toEqual(['"value" must be a string']) expect(validateJsonSchemaValue(asserted({ type: 'number' }), '1')).toEqual(['"value" must be a number']) expect(validateJsonSchemaValue(asserted({ type: 'number' }), Number.NaN)).toEqual(['"value" must be a finite JSON number']) expect(validateJsonSchemaValue(asserted({ type: 'number' }), -0)).toEqual(['"value" must be a finite JSON number']) expect(validateJsonSchemaValue(asserted({ type: 'integer' }), 1.5)).toEqual(['"value" must be an integer']) expect(validateJsonSchemaValue(asserted({ type: 'boolean' }), 'true')).toEqual(['"value" must be a boolean']) expect(validateJsonSchemaValue(asserted({ type: 'null' }), 0)).toEqual(['"value" must be null']) }) it('enforces scalar enum and const together', () => { const schema = asserted({ type: 'string', enum: ['a', 'b'], const: 'a' }) expect(validateJsonSchemaValue(schema, 'a')).toEqual([]) expect(validateJsonSchemaValue(schema, 'c')).toEqual(['"value" must be one of ["a","b"]']) expect(validateJsonSchemaValue(schema, 'b')).toEqual(['"value" must be "a"']) }) it('validates object requiredness, nested values, and raw open defaults', () => { const open = asserted({ type: 'object', properties: { file: { type: 'string' }, nested: { type: 'object', properties: { line: { type: 'integer' } }, required: ['line'], additionalProperties: false, }, }, required: ['file'], }) expect(validateJsonSchemaValue(open, { file: 'a', extra: [1], nested: { line: 2 } })).toEqual([]) expect(validateJsonSchemaValue(open, { nested: { line: 1 } })) .toEqual(['missing required property "value.file"']) expect(validateJsonSchemaValue(open, { file: 1, nested: {} })).toEqual([ '"value.file" must be a string', 'missing required property "value.nested.line"', ]) expect(validateJsonSchemaValue(open, { file: 'a', nested: { line: 1, extra: true } })) .toEqual(['"value.nested.extra" is not a declared property (additionalProperties: false)']) expect(validateJsonSchemaValue(open, 'x')).toEqual(['"value" must be an object']) }) it('treats present undefined as missing when required, then rejects other lossy objects', () => { const required = asserted({ type: 'object', properties: { x: {} }, required: ['x'] }) expect(validateJsonSchemaValue(required, { x: undefined })) .toEqual(['missing required property "value.x"']) expect(validateJsonSchemaValue(asserted({ type: 'object' }), { x: undefined })) .toEqual(['"value" must be a lossless JSON object']) expect(validateJsonSchemaValue(asserted({ type: 'object' }), new Date(0))) .toEqual(['"value" must be an object']) }) it('returns a violation instead of throwing for a container with a hostile getter', () => { const value = Object.defineProperty({}, 'answer', { enumerable: true, get() { throw new Error('getter exploded') }, }) const schema = asserted({ type: 'object', properties: { answer: { type: 'integer' } }, required: ['answer'], }) expect(validateJsonSchemaValue(schema, value)) .toEqual(['"value" must be a lossless JSON value']) }) it('validates dense arrays per index and rejects lossy arrays', () => { const schema = asserted({ type: 'array', items: { type: 'integer' } }) expect(validateJsonSchemaValue(schema, [1, 2])).toEqual([]) expect(validateJsonSchemaValue(schema, runInNewContext('[1, 2]'))).toEqual([]) expect(validateJsonSchemaValue(schema, [1, 1.5])).toEqual(['"value[1]" must be an integer']) expect(validateJsonSchemaValue(schema, 'x')).toEqual(['"value" must be an array']) const sparse: number[] = [] sparse.length = 2 sparse[0] = 1 expect(validateJsonSchemaValue(schema, sparse)).toEqual(['"value" must be a dense lossless JSON array']) }) it('validates exact-one oneOf semantics, including overlap', () => { const disjoint = asserted({ oneOf: [{ type: 'string' }, { type: 'number' }] }) expect(validateJsonSchemaValue(disjoint, 'x')).toEqual([]) expect(validateJsonSchemaValue(disjoint, null)) .toEqual(['"value" must match exactly one oneOf branch (matched 0)']) const overlap = asserted({ oneOf: [{ type: 'number' }, { type: 'integer' }] }) expect(validateJsonSchemaValue(overlap, 1)) .toEqual(['"value" must match exactly one oneOf branch (matched 2)']) expect(validateJsonSchemaValue(overlap, 1.5)).toEqual([]) }) it('validates deeply nested exact-one unions without using the JavaScript call stack', () => { const depth = 5_000 let schema: JsonSchemaNode = { type: 'string' } for (let index = 0; index < depth; index++) schema = { oneOf: [schema, { type: 'null' }] } assertSupportedJsonSchema(schema) expect(validateJsonSchemaValue(schema, 'leaf')).toEqual([]) expect(validateJsonSchemaValue(schema, 42)) .toEqual(['"value" must match exactly one oneOf branch (matched 0)']) }) it('an unconstrained schema accepts only lossless JSON values', () => { const anyJson = asserted({}) for (const value of [null, true, 1, 'x', [1], { x: null }]) { expect(validateJsonSchemaValue(anyJson, value), JSON.stringify(value)).toEqual([]) } for (const value of [undefined, () => 1, Number.POSITIVE_INFINITY, -0, new Map()]) { expect(validateJsonSchemaValue(anyJson, value)).toEqual(['"value" must be a lossless JSON value']) } const cyclic: Record = {} cyclic.self = cyclic expect(validateJsonSchemaValue(anyJson, cyclic)).toEqual(['"value" must be a lossless JSON value']) const explosive = new Proxy({}, { ownKeys() { throw new Error('value trap') }, }) expect(validateJsonSchemaValue(anyJson, explosive)).toEqual(['"value" must be a lossless JSON value']) }) it('uses own properties for requiredness, recursion, and closed-object checks', () => { expect(validateJsonSchemaValue( asserted({ type: 'object', properties: { toString: { type: 'string' } }, required: ['toString'] }), {}, )).toEqual(['missing required property "value.toString"']) expect(validateJsonSchemaValue(asserted({ type: 'object', additionalProperties: false }), { toString: 1 })) .toEqual(['"value.toString" is not a declared property (additionalProperties: false)']) expect(validateJsonSchemaValue( asserted({ type: 'object', properties: { constructor: { type: 'string' } } }), {}, )).toEqual([]) const inheritedUnion = Object.assign( Object.create({ oneOf: [{ type: 'string' }, { type: 'null' }] }) as JsonSchemaNode, { type: 'object' as const }, ) expect(validateJsonSchemaValue(inheritedUnion, {})).toEqual([]) expect(validateJsonSchemaValue(inheritedUnion, 'x')).toEqual(['"value" must be an object']) expect(validateJsonSchemaValue( { type: 'object', properties: undefined } as unknown as JsonSchemaNode, {}, )).toEqual([]) expect(validateJsonSchemaValue( { type: 'object', required: undefined } as unknown as JsonSchemaNode, {}, )).toEqual([]) }) it('keeps assertNever as a forged-schema backstop', () => { const forged = { type: 'tuple' } as unknown as JsonSchemaNode expect(() => validateJsonSchemaValue(forged, 1)).toThrow(/tuple/) }) })