diff --git a/README.md b/README.md index 1a6ab6c..ed0ce5e 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ * 13 built-in validators * custom validators * nested objects & array handling +* strict object validation * multiple validators * customizable error messages * programmable error object @@ -155,6 +156,19 @@ v.validate({ name: "John" }, schema); // Valid v.validate({ age: 42 }, schema); // Fail ``` +# Strict validation +Object properties which are not specified on the schema are ignored by default. If you set the `$$strict` option to `true` any aditional properties will result in an `strictObject` error. + +```js +let schema = { + name: { type: "string" }, // required + $$strict: true // no additional properties allowed +} + +v.validate({ name: "John" }, schema); // Valid +v.validate({ name: "John", age: 42 }, schema); // Fail +``` + # Multiple validators It is possible to define more validators for a field. In this case, only one validator needs to succeed for the field to be valid. @@ -409,6 +423,10 @@ v.validate({ }, schema); // Fail ("The 'address.zip' field is required!") ``` +### Properties +Property | Default | Description +-------- | -------- | ----------- +`strict` | `false`| if `true` any properties which are not defined on the schema will throw an error. ## `string` This is a `String`. diff --git a/lib/messages.js b/lib/messages.js index 4e2e54b..6a3f8ab 100644 --- a/lib/messages.js +++ b/lib/messages.js @@ -49,6 +49,8 @@ module.exports = { enumValue: "The '{field} field value '{expected}' does not match any of the allowed values!", + object: "The '{field}' must be an Object!", + objectStrict: "The object '{field}' contains invalid keys: '{actual}'!", uuid: "The {field} field must be a valid UUID", uuidVersion: "The {field} field must be a valid version provided", }; \ No newline at end of file diff --git a/lib/rules/object.js b/lib/rules/object.js index 43c01ee..9b7be29 100644 --- a/lib/rules/object.js +++ b/lib/rules/object.js @@ -1,9 +1,24 @@ "use strict"; -module.exports = function checkObject(value) { +module.exports = function checkObject(value, schema) { if (typeof value !== "object" || value === null || Array.isArray(value)) { return this.makeError("object"); } + if (schema.strict === true && schema.props) { + const allowedProps = Object.keys(schema.props); + const invalidProps = []; + const props = Object.keys(value); + + for (let i = 0; i < props.length; i++) { + if (allowedProps.indexOf(props[i]) === -1) { + invalidProps.push(props[i]); + } + } + if (invalidProps.length !== 0) { + return this.makeError("objectStrict", undefined, invalidProps.join(", ")); + } + } + return true; }; \ No newline at end of file diff --git a/lib/validator.js b/lib/validator.js index f1809f8..436a6f1 100644 --- a/lib/validator.js +++ b/lib/validator.js @@ -115,6 +115,9 @@ Validator.prototype.compileSchemaObject = function(schemaObject) { throw new Error("Invalid schema!"); } + const strict = schemaObject.$$strict; + delete schemaObject.$$strict; + let compiledObject = this.cache.get(schemaObject); if (compiledObject) { compiledObject.cycle = true; @@ -133,6 +136,11 @@ Validator.prototype.compileSchemaObject = function(schemaObject) { sourceCode.push("let res;"); sourceCode.push("let propertyPath;"); sourceCode.push("const errors = [];"); + + if (strict === true) { + sourceCode.push("const givenProps = new Map(Object.keys(value).map(key => [key, true]));"); + } + for (let i = 0; i < compiledObject.properties.length; i++) { const property = compiledObject.properties[i]; const name = escapeEvalString(property.name); @@ -147,6 +155,16 @@ Validator.prototype.compileSchemaObject = function(schemaObject) { sourceCode.push("if (res !== true) {"); sourceCode.push(`\tthis.handleResult(errors, propertyPath, res, properties[${i}].compiledType.messages);`); sourceCode.push("}"); + + if (strict === true) { + sourceCode.push(`givenProps.delete("${name}");`); + } + } + + if (strict === true) { + sourceCode.push("if (givenProps.size !== 0) {"); + sourceCode.push("\tthis.handleResult(errors, path || 'rootObject', this.makeError('objectStrict', undefined, [...givenProps.keys()].join(', ')), this.messages);"); + sourceCode.push("}"); } sourceCode.push("return errors.length === 0 ? true : errors;"); diff --git a/test/messages.spec.js b/test/messages.spec.js index ae6aabb..251c6b0 100644 --- a/test/messages.spec.js +++ b/test/messages.spec.js @@ -39,6 +39,8 @@ describe("Test Messages", () => { expect(msg.forbidden).toBeDefined(); expect(msg.email).toBeDefined(); expect(msg.url).toBeDefined(); + expect(msg.object).toBeDefined(); + expect(msg.objectStrict).toBeDefined(); expect(msg.uuid).toBeDefined(); expect(msg.uuidVersion).toBeDefined(); diff --git a/test/rules/object.spec.js b/test/rules/object.spec.js index dcacb35..55b357e 100644 --- a/test/rules/object.spec.js +++ b/test/rules/object.spec.js @@ -11,7 +11,9 @@ describe("Test checkObject", () => { it("should check values", () => { const s = { type: "object" }; const err = { type: "object" }; - + const strict = { type: "object", strict: true, props: { a: "string" } }; + const strictErr = {type: "objectStrict" }; + expect(check(null, s)).toEqual(err); expect(check(undefined, s)).toEqual(err); expect(check(0, s)).toEqual(err); @@ -21,5 +23,7 @@ describe("Test checkObject", () => { expect(check(true, s)).toEqual(err); expect(check([], s)).toEqual(err); expect(check({}, s)).toEqual(true); + expect(check({a: "string"}, strict)).toEqual(true); + expect(check({a: "string", b: "string"}, strict)).toMatchObject(strictErr); }); }); diff --git a/test/validator.spec.js b/test/validator.spec.js index 7d0a515..2676cf3 100644 --- a/test/validator.spec.js +++ b/test/validator.spec.js @@ -1209,3 +1209,60 @@ describe("Test irregular object property names", () => { expect(res).toBe(true); }); }); + +describe("Test $$strict schema restriction on root-level", () => { + const v = new Validator(); + + let schema = { + name: "string", + $$strict: true + }; + + let check = v.compile(schema); + + it("should give error if the object contains additional properties on the root-level", () => { + let obj = { + name: "test", + additionalProperty: "additional" + }; + + let res = check(obj); + + expect(res).toBeInstanceOf(Array); + expect(res.length).toBe(1); + expect(res[0].field).toBe("rootObject"); + expect(res[0].type).toBe("objectStrict"); + }); +}); + +describe("Test $$strict schema restriction on sub-level", () => { + const v = new Validator(); + + let schema = { + address: { + type: "object", + props: { + street: "string", + $$strict: true + } + } + }; + + let check = v.compile(schema); + + it("should give error if the object contains additional properties on the sub-level", () => { + let obj = { + address: { + street: "test", + additionalProperty: "additional" + } + }; + + let res = check(obj); + + expect(res).toBeInstanceOf(Array); + expect(res.length).toBe(1); + expect(res[0].field).toBe("address"); + expect(res[0].type).toBe("objectStrict"); + }); +});