Skip to content

Commit 4a4211b

Browse files
committed
Add more options to stripUnknown
Fixes #903, #904, and probably many others.
1 parent d1b673d commit 4a4211b

File tree

7 files changed

+153
-41
lines changed

7 files changed

+153
-41
lines changed

API.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,11 @@ Validates a value using the given schema and options where:
134134
- `convert` - when `true`, attempts to cast values to the required types (e.g. a string to a number). Defaults to `true`.
135135
- `allowUnknown` - when `true`, allows object to contain unknown keys which are ignored. Defaults to `false`.
136136
- `skipFunctions` - when `true`, ignores unknown keys with a function value. Defaults to `false`.
137-
- `stripUnknown` - when `true`, unknown keys are deleted (only when value is an object or an array). Defaults to `false`.
137+
- `stripUnknown` - remove unknown elements from objects and arrays. Defaults to `false`.
138+
- when `true`, all unknown elements will be removed.
139+
- when an `object` :
140+
- `arrays` - set to `true` to remove unknown items from arrays.
141+
- `objects` - set to `true` to remove unknown keys from objects.
138142
- `language` - overrides individual error messages. Defaults to no override (`{}`). Messages apply the following rules :
139143
- variables are put between curly braces like `{{var}}`, if prefixed by a `!` like `{{!var}}`, it will be html escaped
140144
- strings are always preceeded by the key name, unless a `{{key}}` is found elsewhere or if the string is prefixed by a `!!`

lib/any.js

+20-32
Original file line numberDiff line numberDiff line change
@@ -32,38 +32,26 @@ internals.defaults = {
3232

3333
internals.checkOptions = function (options) {
3434

35-
const optionType = {
36-
abortEarly: 'boolean',
37-
convert: 'boolean',
38-
allowUnknown: 'boolean',
39-
skipFunctions: 'boolean',
40-
stripUnknown: 'boolean',
41-
language: 'object',
42-
presence: ['string', 'required', 'optional', 'forbidden', 'ignore'],
43-
raw: 'boolean',
44-
context: 'object',
45-
strip: 'boolean',
46-
noDefaults: 'boolean',
47-
error: 'object'
48-
};
49-
50-
const keys = Object.keys(options);
51-
for (let i = 0; i < keys.length; ++i) {
52-
const key = keys[i];
53-
const opt = optionType[key];
54-
let type = opt;
55-
let values = null;
56-
57-
if (Array.isArray(opt)) {
58-
type = opt[0];
59-
values = opt.slice(1);
60-
}
61-
62-
Hoek.assert(type, 'unknown key ' + key);
63-
Hoek.assert(typeof options[key] === type, key + ' should be of type ' + type);
64-
if (values) {
65-
Hoek.assert(values.indexOf(options[key]) >= 0, key + ' should be one of ' + values.join(', '));
66-
}
35+
const Joi = require('../');
36+
37+
const optionsSchema = Joi.object({
38+
abortEarly: Joi.boolean(),
39+
convert: Joi.boolean(),
40+
allowUnknown: Joi.boolean(),
41+
skipFunctions: Joi.boolean(),
42+
stripUnknown: [Joi.boolean(), Joi.object({ arrays: Joi.boolean(), objects: Joi.boolean() }).or('arrays', 'objects')],
43+
language: Joi.object(),
44+
presence: Joi.string().only('required', 'optional', 'forbidden', 'ignore'),
45+
raw: Joi.boolean(),
46+
context: Joi.object(),
47+
strip: Joi.boolean(),
48+
noDefaults: Joi.boolean(),
49+
error: Joi.object()
50+
}).strict();
51+
52+
const result = optionsSchema.validate(options);
53+
if (result.error) {
54+
throw new Error(result.error.details[0].message);
6755
}
6856
};
6957

lib/array.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,10 @@ internals.checkItems = function (items, wasArray, state, options) {
206206

207207
// Inclusions
208208

209+
const stripUnknown = options.stripUnknown
210+
? (options.stripUnknown === true ? true : !!options.stripUnknown.arrays)
211+
: false;
212+
209213
jl = inclusions.length;
210214
for (let j = 0; j < jl; ++j) {
211215
const inclusion = inclusions[j];
@@ -234,7 +238,7 @@ internals.checkItems = function (items, wasArray, state, options) {
234238

235239
// Return the actual error if only one inclusion defined
236240
if (jl === 1) {
237-
if (options.stripUnknown) {
241+
if (stripUnknown) {
238242
internals.fastSplice(items, i);
239243
--i;
240244
--il;
@@ -258,7 +262,7 @@ internals.checkItems = function (items, wasArray, state, options) {
258262
}
259263

260264
if (this._inner.inclusions.length && !isValid) {
261-
if (options.stripUnknown) {
265+
if (stripUnknown) {
262266
internals.fastSplice(items, i);
263267
--i;
264268
--il;

lib/object.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,15 @@ internals.Object.prototype._base = function (value, state, options) {
217217
if (options.stripUnknown ||
218218
options.skipFunctions) {
219219

220+
const stripUnknown = options.stripUnknown
221+
? (options.stripUnknown === true ? true : !!options.stripUnknown.objects)
222+
: false;
223+
224+
220225
for (let i = 0; i < unprocessedKeys.length; ++i) {
221226
const key = unprocessedKeys[i];
222227

223-
if (options.stripUnknown) {
228+
if (stripUnknown) {
224229
delete target[key];
225230
delete unprocessed[key];
226231
}

test/any.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ describe('any', () => {
110110
expect(() => {
111111

112112
Joi.any().options({ foo: 'bar' });
113-
}).to.throw('unknown key foo');
113+
}).to.throw('"foo" is not allowed');
114114
done();
115115
});
116116

@@ -119,7 +119,7 @@ describe('any', () => {
119119
expect(() => {
120120

121121
Joi.any().options({ convert: 'yes' });
122-
}).to.throw('convert should be of type boolean');
122+
}).to.throw('"convert" must be a boolean');
123123
done();
124124
});
125125

@@ -128,7 +128,7 @@ describe('any', () => {
128128
expect(() => {
129129

130130
Joi.any().options({ presence: 'yes' });
131-
}).to.throw('presence should be one of required, optional, forbidden, ignore');
131+
}).to.throw('"presence" must be one of [required, optional, forbidden, ignore]');
132132
done();
133133
});
134134

test/array.js

+21
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,16 @@ describe('array', () => {
166166
], done);
167167
});
168168

169+
it('validates multiple types with stripUnknown (as an object)', (done) => {
170+
171+
const schema = Joi.array().items(Joi.number(), Joi.string()).options({ stripUnknown: { arrays: true, objects: false } });
172+
173+
Helper.validate(schema, [
174+
[[1, 2, 'a'], true, null, [1, 2, 'a']],
175+
[[1, { foo: 'bar' }, 'a', 2], true, null, [1, 'a', 2]]
176+
], done);
177+
});
178+
169179
it('allows forbidden to restrict values', (done) => {
170180

171181
const schema = Joi.array().items(Joi.string().valid('four').forbidden(), Joi.string());
@@ -820,6 +830,17 @@ describe('array', () => {
820830
done();
821831
});
822832
});
833+
834+
it('respects stripUnknown (as an object)', (done) => {
835+
836+
const schema = Joi.array().items(Joi.string()).options({ stripUnknown: { arrays: true, objects: false } });
837+
schema.validate(['one', 'two', 3, 4, true, false], (err, value) => {
838+
839+
expect(err).to.not.exist();
840+
expect(value).to.deep.equal(['one', 'two']);
841+
done();
842+
});
843+
});
823844
});
824845

825846
describe('ordered()', () => {

test/index.js

+92-2
Original file line numberDiff line numberDiff line change
@@ -1073,6 +1073,28 @@ describe('Joi', () => {
10731073
});
10741074
});
10751075

1076+
it('validates with extra keys and remove them when stripUnknown (as an object) is set', (done) => {
1077+
1078+
const schema = {
1079+
a: Joi.number().min(0).max(3),
1080+
b: Joi.string().valid('a', 'b', 'c'),
1081+
c: Joi.string().email().optional()
1082+
};
1083+
1084+
const obj = {
1085+
a: 1,
1086+
b: 'a',
1087+
d: 'c'
1088+
};
1089+
1090+
Joi.validate(obj, schema, { stripUnknown: { arrays: false, objects: true }, allowUnknown: true }, (err, value) => {
1091+
1092+
expect(err).to.be.null();
1093+
expect(value).to.deep.equal({ a: 1, b: 'a' });
1094+
done();
1095+
});
1096+
});
1097+
10761098
it('validates dependencies when stripUnknown is set', (done) => {
10771099

10781100
const schema = Joi.object({
@@ -1093,7 +1115,27 @@ describe('Joi', () => {
10931115
});
10941116
});
10951117

1096-
it('fails to validate with incorrect property when asked to strip unkown keys without aborting early', (done) => {
1118+
it('validates dependencies when stripUnknown (as an object) is set', (done) => {
1119+
1120+
const schema = Joi.object({
1121+
a: Joi.number(),
1122+
b: Joi.string()
1123+
}).and('a', 'b');
1124+
1125+
const obj = {
1126+
a: 1,
1127+
foo: 'bar'
1128+
};
1129+
1130+
Joi.validate(obj, schema, { stripUnknown: { arrays: false, objects: true } }, (err, value) => {
1131+
1132+
expect(err).to.exist();
1133+
expect(err.message).to.equal('"value" contains [a] without its required peers [b]');
1134+
done();
1135+
});
1136+
});
1137+
1138+
it('fails to validate with incorrect property when asked to strip unknown keys without aborting early', (done) => {
10971139

10981140
const schema = {
10991141
a: Joi.number().min(0).max(3),
@@ -1114,6 +1156,27 @@ describe('Joi', () => {
11141156
});
11151157
});
11161158

1159+
it('fails to validate with incorrect property when asked to strip unknown keys (as an object) without aborting early', (done) => {
1160+
1161+
const schema = {
1162+
a: Joi.number().min(0).max(3),
1163+
b: Joi.string().valid('a', 'b', 'c'),
1164+
c: Joi.string().email().optional()
1165+
};
1166+
1167+
const obj = {
1168+
a: 1,
1169+
b: 'f',
1170+
d: 'c'
1171+
};
1172+
1173+
Joi.validate(obj, schema, { stripUnknown: { arrays: false, objects: true }, abortEarly: false }, (err, value) => {
1174+
1175+
expect(err).to.exist();
1176+
done();
1177+
});
1178+
});
1179+
11171180
it('should pass validation with extra keys when allowUnknown is set', (done) => {
11181181

11191182
const schema = {
@@ -1164,7 +1227,7 @@ describe('Joi', () => {
11641227
});
11651228

11661229

1167-
it('should pass validation with extra keys and remove them when skipExtraKeys is set locally', (done) => {
1230+
it('should pass validation with extra keys and remove them when stripUnknown is set locally', (done) => {
11681231

11691232
const localConfig = Joi.object({
11701233
a: Joi.number().min(0).max(3),
@@ -1191,6 +1254,33 @@ describe('Joi', () => {
11911254
});
11921255
});
11931256

1257+
it('should pass validation with extra keys and remove them when stripUnknown (as an object) is set locally', (done) => {
1258+
1259+
const localConfig = Joi.object({
1260+
a: Joi.number().min(0).max(3),
1261+
b: Joi.string().valid('a', 'b', 'c')
1262+
}).options({ stripUnknown: { arrays: false, objects: true }, allowUnknown: true });
1263+
1264+
const obj = {
1265+
a: 1,
1266+
b: 'a',
1267+
d: 'c'
1268+
};
1269+
1270+
localConfig.validate(obj, (err, value) => {
1271+
1272+
expect(err).to.be.null();
1273+
expect(value).to.deep.equal({ a: 1, b: 'a' });
1274+
1275+
localConfig.validate(value, (err2, value2) => {
1276+
1277+
expect(err2).to.be.null();
1278+
expect(value2).to.deep.equal({ a: 1, b: 'a' });
1279+
done();
1280+
});
1281+
});
1282+
});
1283+
11941284
it('should work when the skipFunctions setting is enabled', (done) => {
11951285

11961286
const schema = Joi.object({ username: Joi.string() }).options({ skipFunctions: true });

0 commit comments

Comments
 (0)