Repository: icebob/fastest-validator
Pull request #47 (2018-11-12 17:50:48)

Strict object validation
I propose the addition of strict object validation:
Validate objects to only allow fields that are referenced in the props dictionary of an object.
```js
const schema = {
    obj: {
        type: "object",
        props: {
            a: "string"
        },
        strict: true
    }
};

console.log(v.validate({ obj: { a: "string" } }, schema));
// Returns: true

console.log(v.validate({ obj: { a: "string", b: "string" } }, schema));
/* Returns an array with errors:
[ { type: 'objectStrict',
    expected: 'b',
    actual: undefined,
    field: 'obj',
    message: 'The object \'obj\' contains invalid keys: b!' } ]
*/
```

Perhaps someone has a better name than "strict object validation" for this.

The performance impact with `npm run bench` is not measurable, but adding a separate "strictObject" validator is certainly possible if this is a problem.


Patch:
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;");


Test patch:
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");
+	});
+});