Skip to content

Commit ab4a81c

Browse files
committed
fix: fix min/maxProperties in Ajv validation
1 parent 104fa5e commit ab4a81c

File tree

3 files changed

+58
-2
lines changed

3 files changed

+58
-2
lines changed

packages/nodejs-lib/src/validation/ajv/ajv.validations.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2642,7 +2642,15 @@ describe('object', () => {
26422642
expect(err, _stringify(value)).toBeNull()
26432643
})
26442644

2645-
const invalidCases: any[] = [{ foo: 'foo' }, {}, 0, 'abcd', true, []]
2645+
const invalidCases: any[] = [
2646+
{ foo: 'foo' },
2647+
{},
2648+
0,
2649+
'abcd',
2650+
true,
2651+
[],
2652+
{ abc: 'abc', def: 'def' },
2653+
]
26462654
invalidCases.forEach(value => {
26472655
const [err] = ajvSchema.getValidationResult(value)
26482656
expect(err, _stringify(value)).not.toBeNull()
@@ -2861,6 +2869,33 @@ describe('object', () => {
28612869
'The schema must be type checked against a type or interface, using the `.isOfType()` helper in `j`.',
28622870
)
28632871
})
2872+
2873+
describe('minProperties', () => {
2874+
test('should work for withEnumKeys as well', () => {
2875+
enum E {
2876+
A = 'a',
2877+
B = 'b',
2878+
}
2879+
const schema = j.object
2880+
.withEnumKeys(E, j.number().optional())
2881+
.minProperties(1)
2882+
.isOfType<Record<E, number | undefined>>()
2883+
const ajvSchema = AjvSchema.create(schema)
2884+
2885+
const validCases: any[] = [{ a: 1 }, { a: 1, b: 2 }]
2886+
for (const data of validCases) {
2887+
const [err, result] = ajvSchema.getValidationResult(data)
2888+
expect(err, _stringify(data)).toBeNull()
2889+
expect(result, _stringify(data)).toEqual(data)
2890+
}
2891+
2892+
const invalidCases: any[] = [{}, { a: '1' }, { c: 1 }]
2893+
for (const data of invalidCases) {
2894+
const [err] = ajvSchema.getValidationResult(data)
2895+
expect(err, _stringify(data)).not.toBeNull()
2896+
}
2897+
})
2898+
})
28642899
})
28652900

28662901
describe('.stringMap', () => {

packages/nodejs-lib/src/validation/ajv/getAjv.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,26 @@ export function createAjv(opt?: Options): Ajv2020 {
478478
},
479479
})
480480

481+
// This and `maxProperties2` are added because Ajv validates the `min/maxProperties` before validating the properties.
482+
// So, in case of `minProperties(1)` and `{ foo: 'bar' }` Ajv will let it pass, even
483+
// if the property validation would strip `foo` from the data.
484+
// And Ajv would return `{}` as a successful validation.
485+
// Since the keyword validation runs after JSON Schema validation,
486+
// here we can make sure the number of properties are right ex-post property validation.
487+
// It's named with the `2` suffix, because `minProperties` is reserved.
488+
// And `maxProperties` does not suffer from this error due to the nature of how maximum works.
489+
ajv.addKeyword({
490+
keyword: 'minProperties2',
491+
type: 'object',
492+
modifying: false,
493+
errors: true,
494+
schemaType: 'number',
495+
validate: function validate(minProperties: number, data: AnyObject, _schema, _ctx) {
496+
if (typeof data !== 'object') return true
497+
return Object.getOwnPropertyNames(data).length >= minProperties
498+
},
499+
})
500+
481501
return ajv
482502
}
483503

packages/nodejs-lib/src/validation/ajv/jsonSchemaBuilder.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -920,7 +920,7 @@ export class JsonSchemaObjectBuilder<
920920
}
921921

922922
minProperties(minProperties: number): this {
923-
return this.cloneAndUpdateSchema({ minProperties })
923+
return this.cloneAndUpdateSchema({ minProperties, minProperties2: minProperties })
924924
}
925925

926926
maxProperties(maxProperties: number): this {
@@ -1239,6 +1239,7 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
12391239
errorMessages?: StringMap<string>
12401240
optionalValues?: (string | number | boolean)[]
12411241
keySchema?: JsonSchema
1242+
minProperties2?: number
12421243
}
12431244

12441245
function object(props: AnyObject): never

0 commit comments

Comments
 (0)