Skip to content

Commit 5fe01c3

Browse files
authored
Merge pull request #317 from Freezystem/fix/consider-null-a-value
consider null as a value
2 parents 2023aa4 + 9e81f1a commit 5fe01c3

File tree

9 files changed

+603
-166
lines changed

9 files changed

+603
-166
lines changed

README.md

+26
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,32 @@ object2.about // is null
191191

192192
check({ about: "Custom" }) // Valid
193193
```
194+
### Considering `null` as a value
195+
In specific case, you may want to consider `null` as a valid input even for a `required` field.
196+
197+
It's useful in cases you want a field to be:
198+
- `required` and `null` without specifying `nullable: true` in its definition.
199+
- `required` and not `null` by specifying `nullable: false` in its definition.
200+
- `optional` **but specifically not** `null`.
201+
202+
To be able to achieve this you'll have to set the `considerNullAsAValue` validator option to `true`.
203+
```js
204+
const v = new Validator({considerNullAsAValue: true});
205+
206+
const schema = {foo: {type: "number"}, bar: {type: "number", optional: true, nullable: false}, baz: {type: "number", nullable: false}};
207+
const check = v.compile(schema);
208+
209+
const object1 = {foo: null, baz: 1};
210+
check(object1); // valid (foo is required and can be null)
211+
212+
const object2 = {foo: 3, bar: null, baz: 1};
213+
check(object2); // not valid (bar is optional but can't be null)
214+
215+
const object3 = {foo: 3, baz: null};
216+
check(object3); // not valid (baz is required but can't be null)
217+
218+
```
219+
With this option set all fields will be considered _nullable_ by default.
194220

195221
# Strict validation
196222
Object properties which are not specified on the schema are ignored by default. If you set the `$$strict` option to `true` any additional properties will result in an `strictObject` error.

index.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,11 @@ export interface ValidatorConstructorOptions {
907907
*/
908908
useNewCustomCheckerFunction?: boolean;
909909

910+
/**
911+
* consider null as a value?
912+
*/
913+
considerNullAsAValue?: boolean;
914+
910915
/**
911916
* Default settings for rules
912917
*/

lib/validator.js

+19-7
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,26 @@ class Validator {
113113
*/
114114
wrapRequiredCheckSourceCode(rule, innerSrc, context, resVar) {
115115
const src = [];
116+
const {considerNullAsAValue = false} = this.opts;
116117
let handleNoValue;
117118

118119
let skipUndefinedValue = rule.schema.optional === true || rule.schema.type === "forbidden";
119-
let skipNullValue = rule.schema.optional === true || rule.schema.nullable === true || rule.schema.type === "forbidden";
120+
let skipNullValue = considerNullAsAValue ?
121+
rule.schema.nullable !== false || rule.schema.type === "forbidden" :
122+
rule.schema.optional === true || rule.schema.nullable === true || rule.schema.type === "forbidden";
120123

121-
if (rule.schema.default != null) {
124+
const ruleHasDefault = considerNullAsAValue ?
125+
rule.schema.default != undefined && rule.schema.default != null :
126+
rule.schema.default != undefined;
127+
128+
if (ruleHasDefault) {
122129
// We should set default-value when value is undefined or null, not skip! (Except when null is allowed)
123130
skipUndefinedValue = false;
124-
if (rule.schema.nullable !== true) skipNullValue = false;
131+
if (considerNullAsAValue) {
132+
if (rule.schema.nullable === false) skipNullValue = false;
133+
} else {
134+
if (rule.schema.nullable !== true) skipNullValue = false;
135+
}
125136

126137
let defaultValue;
127138
if (typeof rule.schema.default === "function") {
@@ -504,11 +515,12 @@ class Validator {
504515
schema.optional = true;
505516

506517
// Check 'nullable' flag
507-
const isNullable = schema.rules
518+
const nullCheck = this.opts.considerNullAsAValue ? false : true;
519+
const setNullable = schema.rules
508520
.map(s => this.getRuleFromSchema(s))
509-
.every(rule => rule.schema.nullable === true);
510-
if (isNullable)
511-
schema.nullable = true;
521+
.every(rule => rule.schema.nullable === nullCheck);
522+
if (setNullable)
523+
schema.nullable = nullCheck;
512524
}
513525

514526
if (schema.$$type) {

test/integration.spec.js

+195-52
Original file line numberDiff line numberDiff line change
@@ -1109,75 +1109,218 @@ describe("Test optional option", () => {
11091109
});
11101110

11111111
describe("Test nullable option", () => {
1112-
const v = new Validator();
1112+
describe("old case", () => {
1113+
const v = new Validator();
11131114

1114-
it("should throw error if value is undefined", () => {
1115-
const schema = { foo: { type: "number", nullable: true } };
1116-
const check = v.compile(schema);
1115+
it("should throw error if value is undefined", () => {
1116+
const schema = { foo: { type: "number", nullable: true } };
1117+
const check = v.compile(schema);
11171118

1118-
expect(check(check)).toBeInstanceOf(Array);
1119-
expect(check({ foo: undefined })).toBeInstanceOf(Array);
1120-
});
1119+
expect(check(check)).toBeInstanceOf(Array);
1120+
expect(check({ foo: undefined })).toBeInstanceOf(Array);
1121+
});
11211122

1122-
it("should not throw error if value is null", () => {
1123-
const schema = { foo: { type: "number", nullable: true } };
1124-
const check = v.compile(schema);
1123+
it("should not throw error if value is null", () => {
1124+
const schema = { foo: { type: "number", nullable: true } };
1125+
const check = v.compile(schema);
11251126

1126-
const o = { foo: null };
1127-
expect(check(o)).toBe(true);
1128-
expect(o.foo).toBe(null);
1129-
});
1127+
const o = { foo: null };
1128+
expect(check(o)).toBe(true);
1129+
expect(o.foo).toBe(null);
1130+
});
11301131

1131-
it("should not throw error if value exist", () => {
1132-
const schema = { foo: { type: "number", nullable: true } };
1133-
const check = v.compile(schema);
1134-
expect(check({ foo: 2 })).toBe(true);
1135-
});
1132+
it("should not throw error if value exist", () => {
1133+
const schema = { foo: { type: "number", nullable: true } };
1134+
const check = v.compile(schema);
1135+
expect(check({ foo: 2 })).toBe(true);
1136+
});
11361137

1137-
it("should set default value if there is a default", () => {
1138-
const schema = { foo: { type: "number", nullable: true, default: 5 } };
1139-
const check = v.compile(schema);
1138+
it("should set default value if there is a default", () => {
1139+
const schema = { foo: { type: "number", nullable: true, default: 5 } };
1140+
const check = v.compile(schema);
11401141

1141-
const o1 = { foo: 2 };
1142-
expect(check(o1)).toBe(true);
1143-
expect(o1.foo).toBe(2);
1142+
const o1 = { foo: 2 };
1143+
expect(check(o1)).toBe(true);
1144+
expect(o1.foo).toBe(2);
11441145

1145-
const o2 = {};
1146-
expect(check(o2)).toBe(true);
1147-
expect(o2.foo).toBe(5);
1148-
});
1146+
const o2 = {};
1147+
expect(check(o2)).toBe(true);
1148+
expect(o2.foo).toBe(5);
1149+
});
11491150

1150-
it("should not set default value if current value is null", () => {
1151-
const schema = { foo: { type: "number", nullable: true, default: 5 } };
1152-
const check = v.compile(schema);
1151+
it("should not set default value if current value is null", () => {
1152+
const schema = { foo: { type: "number", nullable: true, default: 5 } };
1153+
const check = v.compile(schema);
11531154

1154-
const o = { foo: null };
1155-
expect(check(o)).toBe(true);
1156-
expect(o.foo).toBe(null);
1157-
});
1155+
const o = { foo: null };
1156+
expect(check(o)).toBe(true);
1157+
expect(o.foo).toBe(null);
1158+
});
11581159

1159-
it("should work with optional", () => {
1160-
const schema = { foo: { type: "number", nullable: true, optional: true } };
1161-
const check = v.compile(schema);
1160+
it("should work with optional", () => {
1161+
const schema = { foo: { type: "number", nullable: true, optional: true } };
1162+
const check = v.compile(schema);
11621163

1163-
expect(check({ foo: 3 })).toBe(true);
1164-
expect(check({ foo: null })).toBe(true);
1165-
expect(check({})).toBe(true);
1164+
expect(check({ foo: 3 })).toBe(true);
1165+
expect(check({ foo: null })).toBe(true);
1166+
expect(check({})).toBe(true);
1167+
});
1168+
1169+
it("should work with optional and default", () => {
1170+
const schema = { foo: { type: "number", nullable: true, optional: true, default: 5 } };
1171+
const check = v.compile(schema);
1172+
1173+
expect(check({ foo: 3 })).toBe(true);
1174+
1175+
const o1 = { foo: null };
1176+
expect(check(o1)).toBe(true);
1177+
expect(o1.foo).toBe(null);
1178+
1179+
const o2 = {};
1180+
expect(check(o2)).toBe(true);
1181+
expect(o2.foo).toBe(5);
1182+
});
1183+
1184+
it("should accept null value when optional", () => {
1185+
const schema = { foo: { type: "number", nullable: false, optional: true } };
1186+
const check = v.compile(schema);
1187+
1188+
expect(check({ foo: 3 })).toBe(true);
1189+
expect(check({ foo: undefined })).toBe(true);
1190+
expect(check({})).toBe(true);
1191+
expect(check({ foo: null })).toBe(true);
1192+
});
1193+
1194+
it("should accept null as value when required", () => {
1195+
const schema = {foo: {type: "number", nullable: true, optional: false}};
1196+
const check = v.compile(schema);
1197+
1198+
expect(check({ foo: 3 })).toBe(true);
1199+
expect(check({ foo: undefined })).toEqual([{"actual": undefined, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]);
1200+
expect(check({})).toEqual([{"actual": undefined, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]);
1201+
expect(check({ foo: null })).toBe(true);
1202+
});
1203+
1204+
it("should not accept null as value when required and not explicitly not nullable", () => {
1205+
const schema = {foo: {type: "number", optional: false}};
1206+
const check = v.compile(schema);
1207+
1208+
expect(check({ foo: 3 })).toBe(true);
1209+
expect(check({ foo: undefined })).toEqual([{"actual": undefined, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]);
1210+
expect(check({})).toEqual([{"actual": undefined, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]);
1211+
expect(check({ foo: null })).toEqual([{"actual": null, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]);
1212+
});
11661213
});
11671214

1168-
it("should work with optional and default", () => {
1169-
const schema = { foo: { type: "number", nullable: true, optional: true, default: 5 } };
1170-
const check = v.compile(schema);
1215+
describe("new case (with considerNullAsAValue flag set to true)", () => {
1216+
const v = new Validator({considerNullAsAValue: true});
11711217

1172-
expect(check({ foo: 3 })).toBe(true);
1218+
it("should throw error if value is undefined", () => {
1219+
const schema = { foo: { type: "number" } };
1220+
const check = v.compile(schema);
11731221

1174-
const o1 = { foo: null };
1175-
expect(check(o1)).toBe(true);
1176-
expect(o1.foo).toBe(null);
1222+
expect(check(check)).toBeInstanceOf(Array);
1223+
expect(check({ foo: undefined })).toBeInstanceOf(Array);
1224+
});
11771225

1178-
const o2 = {};
1179-
expect(check(o2)).toBe(true);
1180-
expect(o2.foo).toBe(5);
1226+
it("should not throw error if value is null", () => {
1227+
const schema = { foo: { type: "number" } };
1228+
const check = v.compile(schema);
1229+
1230+
const o = { foo: null };
1231+
expect(check(o)).toBe(true);
1232+
expect(o.foo).toBe(null);
1233+
});
1234+
1235+
it("should not throw error if value exist", () => {
1236+
const schema = { foo: { type: "number" } };
1237+
const check = v.compile(schema);
1238+
expect(check({ foo: 2 })).toBe(true);
1239+
});
1240+
1241+
it("should set default value if there is a default", () => {
1242+
const schema = { foo: { type: "number", default: 5 } };
1243+
const check = v.compile(schema);
1244+
1245+
const o1 = { foo: 2 };
1246+
expect(check(o1)).toBe(true);
1247+
expect(o1.foo).toBe(2);
1248+
1249+
const o2 = {};
1250+
expect(check(o2)).toBe(true);
1251+
expect(o2.foo).toBe(5);
1252+
});
1253+
1254+
it("should not set default value if current value is null", () => {
1255+
const schema = { foo: { type: "number", default: 5 } };
1256+
const check = v.compile(schema);
1257+
1258+
const o = { foo: null };
1259+
expect(check(o)).toBe(true);
1260+
expect(o.foo).toBe(null);
1261+
});
1262+
1263+
it("should set default value if current value is null but can't be", () => {
1264+
const schema = { foo: { type: "number", default: 5, nullable: false } };
1265+
const check = v.compile(schema);
1266+
1267+
const o = { foo: null };
1268+
expect(check(o)).toBe(true);
1269+
expect(o.foo).toBe(5);
1270+
});
1271+
1272+
it("should set default value if current value is null but optional", () => {
1273+
const schema = { foo: { type: "number", default: 5, nullable: false, optional: true } };
1274+
const check = v.compile(schema);
1275+
1276+
const o = { foo: null };
1277+
expect(check(o)).toBe(true);
1278+
expect(o.foo).toBe(5);
1279+
});
1280+
1281+
it("should work with optional", () => {
1282+
const schema = { foo: { type: "number", optional: true } };
1283+
const check = v.compile(schema);
1284+
1285+
expect(check({ foo: 3 })).toBe(true);
1286+
expect(check({ foo: null })).toBe(true);
1287+
expect(check({})).toBe(true);
1288+
});
1289+
1290+
it("should work with optional and default", () => {
1291+
const schema = { foo: { type: "number", optional: true, default: 5 } };
1292+
const check = v.compile(schema);
1293+
1294+
expect(check({ foo: 3 })).toBe(true);
1295+
1296+
const o1 = { foo: null };
1297+
expect(check(o1)).toBe(true);
1298+
expect(o1.foo).toBe(null);
1299+
1300+
const o2 = {};
1301+
expect(check(o2)).toBe(true);
1302+
expect(o2.foo).toBe(5);
1303+
});
1304+
1305+
it("should not accept null value even if optional", () => {
1306+
const schema = { foo: { type: "number", nullable: false, optional: true } };
1307+
const check = v.compile(schema);
1308+
1309+
expect(check({ foo: 3 })).toBe(true);
1310+
expect(check({ foo: undefined })).toBe(true);
1311+
expect(check({})).toBe(true);
1312+
expect(check({ foo: null })).toEqual([{"actual": null, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]);
1313+
});
1314+
1315+
it("should not accept null as value", () => {
1316+
const schema = {foo: {type: "number", nullable: false}};
1317+
const check = v.compile(schema);
1318+
1319+
expect(check({ foo: 3 })).toBe(true);
1320+
expect(check({ foo: undefined })).toEqual([{"actual": undefined, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]);
1321+
expect(check({})).toEqual([{"actual": undefined, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]);
1322+
expect(check({ foo: null })).toEqual([{"actual": null, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]);
1323+
});
11811324
});
11821325
});
11831326

0 commit comments

Comments
 (0)