Skip to content

Commit d4742f9

Browse files
author
Jean Pereira
committed
fix(schema): additionalProperties not working
1 parent 9e1b004 commit d4742f9

File tree

4 files changed

+134
-0
lines changed

4 files changed

+134
-0
lines changed

src/errors/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type SchemaValidationErrorType =
1414
| 'forbidden'
1515
| 'const'
1616
| 'enum'
17+
| 'additionalProperties'
1718
/**
1819
* Schema composition keywords (allOf, anyOf, oneOf, not)
1920
* These keywords apply subschemas in a logical manner according to JSON Schema spec

src/errors/messages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ export function getErrorMessage(
112112
throw new Error('"minContains" is not implemented yet')
113113
case 'maxContains':
114114
throw new Error('"maxContains" is not implemented yet')
115+
case 'additionalProperties':
116+
return 'Additional property is not allowed'
115117
case 'json-logic':
116118
return customErrorMessage || 'The value is not valid'
117119
}

src/validation/schema.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,43 @@ export function validateSchema(
259259
}
260260
}
261261

262+
if (schema.additionalProperties === false && isObjectValue(value)) {
263+
const definedProperties = new Set(Object.keys(schema.properties || {}))
264+
const actualProperties = Object.keys(value)
265+
266+
// Create a set of pattern regexes from patternProperties
267+
const patternRegexes: RegExp[] = []
268+
if (schema.patternProperties) {
269+
for (const pattern of Object.keys(schema.patternProperties)) {
270+
try {
271+
patternRegexes.push(new RegExp(pattern))
272+
}
273+
catch {
274+
// Invalid regex pattern, skip it
275+
console.warn(`Invalid regex pattern in patternProperties: ${pattern}`)
276+
}
277+
}
278+
}
279+
280+
for (const prop of actualProperties) {
281+
if (definedProperties.has(prop)) {
282+
continue
283+
}
284+
285+
const matchesPattern = patternRegexes.some(regex => regex.test(prop))
286+
if (matchesPattern) {
287+
continue
288+
}
289+
290+
errors.push({
291+
path: [...path, prop],
292+
validation: 'additionalProperties',
293+
schema,
294+
value: value[prop],
295+
})
296+
}
297+
}
298+
262299
return [
263300
...errors,
264301
// JSON-schema spec validations
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { JsfObjectSchema } from '../../src/types'
2+
import { describe, expect, it } from '@jest/globals'
3+
import { createHeadlessForm } from '../../src'
4+
5+
describe('additionalProperties validation', () => {
6+
describe('basic additionalProperties: false', () => {
7+
const schema: JsfObjectSchema = {
8+
type: 'object',
9+
properties: {
10+
a: { type: 'integer' },
11+
b: { type: 'string' },
12+
},
13+
additionalProperties: false,
14+
}
15+
const form = createHeadlessForm(schema)
16+
17+
it('allows objects with only defined properties', () => {
18+
expect(form.handleValidation({ a: 1 })).not.toHaveProperty('formErrors')
19+
expect(form.handleValidation({ a: 1, b: 'test' })).not.toHaveProperty('formErrors')
20+
})
21+
22+
it('rejects objects with additional properties', () => {
23+
expect(form.handleValidation({ a: 1, c: 'extra' })).toMatchObject({
24+
formErrors: { c: 'Additional property is not allowed' },
25+
})
26+
27+
expect(form.handleValidation({ a: 1, b: 'test', c: 'extra' })).toMatchObject({
28+
formErrors: { c: 'Additional property is not allowed' },
29+
})
30+
})
31+
32+
it('rejects objects with only additional properties', () => {
33+
expect(form.handleValidation({ c: 'extra' })).toMatchObject({
34+
formErrors: { c: 'Additional property is not allowed' },
35+
})
36+
})
37+
})
38+
39+
describe('additionalProperties: false with patternProperties', () => {
40+
const schema: JsfObjectSchema = {
41+
type: 'object',
42+
properties: {
43+
foo: {},
44+
bar: {},
45+
},
46+
patternProperties: {
47+
'^v': {},
48+
},
49+
additionalProperties: false,
50+
}
51+
const form = createHeadlessForm(schema)
52+
53+
it('allows properties defined in properties', () => {
54+
expect(form.handleValidation({ foo: 1 })).not.toHaveProperty('formErrors')
55+
expect(form.handleValidation({ foo: 1, bar: 2 })).not.toHaveProperty('formErrors')
56+
})
57+
58+
it('allows properties matching patternProperties', () => {
59+
expect(form.handleValidation({ vroom: 1 })).not.toHaveProperty('formErrors')
60+
expect(form.handleValidation({ vampire: 1 })).not.toHaveProperty('formErrors')
61+
expect(form.handleValidation({ v: 1 })).not.toHaveProperty('formErrors')
62+
})
63+
64+
it('allows combination of properties and patternProperties', () => {
65+
expect(form.handleValidation({ foo: 1, vroom: 2 })).not.toHaveProperty('formErrors')
66+
expect(form.handleValidation({ foo: 1, bar: 2, vampire: 3 })).not.toHaveProperty('formErrors')
67+
})
68+
69+
it('rejects properties that match neither properties nor patternProperties', () => {
70+
expect(form.handleValidation({ foo: 1, quux: 'boom' })).toMatchObject({
71+
formErrors: { quux: 'Additional property is not allowed' },
72+
})
73+
74+
expect(form.handleValidation({ hello: 'world' })).toMatchObject({
75+
formErrors: { hello: 'Additional property is not allowed' },
76+
})
77+
})
78+
})
79+
80+
describe('when additionalProperties is not false', () => {
81+
const schema: JsfObjectSchema = {
82+
type: 'object',
83+
properties: {
84+
a: { type: 'integer' },
85+
},
86+
// additionalProperties defaults to true/undefined
87+
}
88+
const form = createHeadlessForm(schema)
89+
90+
it('allows additional properties', () => {
91+
expect(form.handleValidation({ a: 1, b: 2, c: 3 })).not.toHaveProperty('formErrors')
92+
})
93+
})
94+
})

0 commit comments

Comments
 (0)