Skip to content

Commit c7b6554

Browse files
authored
Merge pull request #47 from fabioanderegg/strict-object
Strict object validation
2 parents 3f41557 + 55750cf commit c7b6554

File tree

7 files changed

+118
-2
lines changed

7 files changed

+118
-2
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* 13 built-in validators
1717
* custom validators
1818
* nested objects & array handling
19+
* strict object validation
1920
* multiple validators
2021
* customizable error messages
2122
* programmable error object
@@ -155,6 +156,19 @@ v.validate({ name: "John" }, schema); // Valid
155156
v.validate({ age: 42 }, schema); // Fail
156157
```
157158

159+
# Strict validation
160+
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.
161+
162+
```js
163+
let schema = {
164+
name: { type: "string" }, // required
165+
$$strict: true // no additional properties allowed
166+
}
167+
168+
v.validate({ name: "John" }, schema); // Valid
169+
v.validate({ name: "John", age: 42 }, schema); // Fail
170+
```
171+
158172
# Multiple validators
159173
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.
160174

@@ -409,6 +423,10 @@ v.validate({
409423
}, schema); // Fail ("The 'address.zip' field is required!")
410424
```
411425

426+
### Properties
427+
Property | Default | Description
428+
-------- | -------- | -----------
429+
`strict` | `false`| if `true` any properties which are not defined on the schema will throw an error.
412430

413431
## `string`
414432
This is a `String`.

lib/messages.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ module.exports = {
4949

5050
enumValue: "The '{field} field value '{expected}' does not match any of the allowed values!",
5151

52+
object: "The '{field}' must be an Object!",
53+
objectStrict: "The object '{field}' contains invalid keys: '{actual}'!",
5254
uuid: "The {field} field must be a valid UUID",
5355
uuidVersion: "The {field} field must be a valid version provided",
5456
};

lib/rules/object.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
11
"use strict";
22

3-
module.exports = function checkObject(value) {
3+
module.exports = function checkObject(value, schema) {
44
if (typeof value !== "object" || value === null || Array.isArray(value)) {
55
return this.makeError("object");
66
}
77

8+
if (schema.strict === true && schema.props) {
9+
const allowedProps = Object.keys(schema.props);
10+
const invalidProps = [];
11+
const props = Object.keys(value);
12+
13+
for (let i = 0; i < props.length; i++) {
14+
if (allowedProps.indexOf(props[i]) === -1) {
15+
invalidProps.push(props[i]);
16+
}
17+
}
18+
if (invalidProps.length !== 0) {
19+
return this.makeError("objectStrict", undefined, invalidProps.join(", "));
20+
}
21+
}
22+
823
return true;
924
};

lib/validator.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ Validator.prototype.compileSchemaObject = function(schemaObject) {
115115
throw new Error("Invalid schema!");
116116
}
117117

118+
const strict = schemaObject.$$strict;
119+
delete schemaObject.$$strict;
120+
118121
let compiledObject = this.cache.get(schemaObject);
119122
if (compiledObject) {
120123
compiledObject.cycle = true;
@@ -133,6 +136,11 @@ Validator.prototype.compileSchemaObject = function(schemaObject) {
133136
sourceCode.push("let res;");
134137
sourceCode.push("let propertyPath;");
135138
sourceCode.push("const errors = [];");
139+
140+
if (strict === true) {
141+
sourceCode.push("const givenProps = new Map(Object.keys(value).map(key => [key, true]));");
142+
}
143+
136144
for (let i = 0; i < compiledObject.properties.length; i++) {
137145
const property = compiledObject.properties[i];
138146
const name = escapeEvalString(property.name);
@@ -147,6 +155,16 @@ Validator.prototype.compileSchemaObject = function(schemaObject) {
147155
sourceCode.push("if (res !== true) {");
148156
sourceCode.push(`\tthis.handleResult(errors, propertyPath, res, properties[${i}].compiledType.messages);`);
149157
sourceCode.push("}");
158+
159+
if (strict === true) {
160+
sourceCode.push(`givenProps.delete("${name}");`);
161+
}
162+
}
163+
164+
if (strict === true) {
165+
sourceCode.push("if (givenProps.size !== 0) {");
166+
sourceCode.push("\tthis.handleResult(errors, path || 'rootObject', this.makeError('objectStrict', undefined, [...givenProps.keys()].join(', ')), this.messages);");
167+
sourceCode.push("}");
150168
}
151169

152170
sourceCode.push("return errors.length === 0 ? true : errors;");

test/messages.spec.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ describe("Test Messages", () => {
3939
expect(msg.forbidden).toBeDefined();
4040
expect(msg.email).toBeDefined();
4141
expect(msg.url).toBeDefined();
42+
expect(msg.object).toBeDefined();
43+
expect(msg.objectStrict).toBeDefined();
4244
expect(msg.uuid).toBeDefined();
4345
expect(msg.uuidVersion).toBeDefined();
4446

test/rules/object.spec.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ describe("Test checkObject", () => {
1111
it("should check values", () => {
1212
const s = { type: "object" };
1313
const err = { type: "object" };
14-
14+
const strict = { type: "object", strict: true, props: { a: "string" } };
15+
const strictErr = {type: "objectStrict" };
16+
1517
expect(check(null, s)).toEqual(err);
1618
expect(check(undefined, s)).toEqual(err);
1719
expect(check(0, s)).toEqual(err);
@@ -21,5 +23,7 @@ describe("Test checkObject", () => {
2123
expect(check(true, s)).toEqual(err);
2224
expect(check([], s)).toEqual(err);
2325
expect(check({}, s)).toEqual(true);
26+
expect(check({a: "string"}, strict)).toEqual(true);
27+
expect(check({a: "string", b: "string"}, strict)).toMatchObject(strictErr);
2428
});
2529
});

test/validator.spec.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,3 +1209,60 @@ describe("Test irregular object property names", () => {
12091209
expect(res).toBe(true);
12101210
});
12111211
});
1212+
1213+
describe("Test $$strict schema restriction on root-level", () => {
1214+
const v = new Validator();
1215+
1216+
let schema = {
1217+
name: "string",
1218+
$$strict: true
1219+
};
1220+
1221+
let check = v.compile(schema);
1222+
1223+
it("should give error if the object contains additional properties on the root-level", () => {
1224+
let obj = {
1225+
name: "test",
1226+
additionalProperty: "additional"
1227+
};
1228+
1229+
let res = check(obj);
1230+
1231+
expect(res).toBeInstanceOf(Array);
1232+
expect(res.length).toBe(1);
1233+
expect(res[0].field).toBe("rootObject");
1234+
expect(res[0].type).toBe("objectStrict");
1235+
});
1236+
});
1237+
1238+
describe("Test $$strict schema restriction on sub-level", () => {
1239+
const v = new Validator();
1240+
1241+
let schema = {
1242+
address: {
1243+
type: "object",
1244+
props: {
1245+
street: "string",
1246+
$$strict: true
1247+
}
1248+
}
1249+
};
1250+
1251+
let check = v.compile(schema);
1252+
1253+
it("should give error if the object contains additional properties on the sub-level", () => {
1254+
let obj = {
1255+
address: {
1256+
street: "test",
1257+
additionalProperty: "additional"
1258+
}
1259+
};
1260+
1261+
let res = check(obj);
1262+
1263+
expect(res).toBeInstanceOf(Array);
1264+
expect(res.length).toBe(1);
1265+
expect(res[0].field).toBe("address");
1266+
expect(res[0].type).toBe("objectStrict");
1267+
});
1268+
});

0 commit comments

Comments
 (0)