Skip to content

Commit

Permalink
Merge pull request #317 from Freezystem/fix/consider-null-a-value
Browse files Browse the repository at this point in the history
consider null as a value
  • Loading branch information
icebob authored Mar 1, 2023
2 parents 2023aa4 + 9e81f1a commit 5fe01c3
Show file tree
Hide file tree
Showing 9 changed files with 603 additions and 166 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,32 @@ object2.about // is null

check({ about: "Custom" }) // Valid
```
### Considering `null` as a value
In specific case, you may want to consider `null` as a valid input even for a `required` field.

It's useful in cases you want a field to be:
- `required` and `null` without specifying `nullable: true` in its definition.
- `required` and not `null` by specifying `nullable: false` in its definition.
- `optional` **but specifically not** `null`.

To be able to achieve this you'll have to set the `considerNullAsAValue` validator option to `true`.
```js
const v = new Validator({considerNullAsAValue: true});

const schema = {foo: {type: "number"}, bar: {type: "number", optional: true, nullable: false}, baz: {type: "number", nullable: false}};
const check = v.compile(schema);

const object1 = {foo: null, baz: 1};
check(object1); // valid (foo is required and can be null)

const object2 = {foo: 3, bar: null, baz: 1};
check(object2); // not valid (bar is optional but can't be null)

const object3 = {foo: 3, baz: null};
check(object3); // not valid (baz is required but can't be null)

```
With this option set all fields will be considered _nullable_ by default.

# Strict validation
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.
Expand Down
5 changes: 5 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,11 @@ export interface ValidatorConstructorOptions {
*/
useNewCustomCheckerFunction?: boolean;

/**
* consider null as a value?
*/
considerNullAsAValue?: boolean;

/**
* Default settings for rules
*/
Expand Down
26 changes: 19 additions & 7 deletions lib/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,26 @@ class Validator {
*/
wrapRequiredCheckSourceCode(rule, innerSrc, context, resVar) {
const src = [];
const {considerNullAsAValue = false} = this.opts;
let handleNoValue;

let skipUndefinedValue = rule.schema.optional === true || rule.schema.type === "forbidden";
let skipNullValue = rule.schema.optional === true || rule.schema.nullable === true || rule.schema.type === "forbidden";
let skipNullValue = considerNullAsAValue ?
rule.schema.nullable !== false || rule.schema.type === "forbidden" :
rule.schema.optional === true || rule.schema.nullable === true || rule.schema.type === "forbidden";

if (rule.schema.default != null) {
const ruleHasDefault = considerNullAsAValue ?
rule.schema.default != undefined && rule.schema.default != null :
rule.schema.default != undefined;

if (ruleHasDefault) {
// We should set default-value when value is undefined or null, not skip! (Except when null is allowed)
skipUndefinedValue = false;
if (rule.schema.nullable !== true) skipNullValue = false;
if (considerNullAsAValue) {
if (rule.schema.nullable === false) skipNullValue = false;
} else {
if (rule.schema.nullable !== true) skipNullValue = false;
}

let defaultValue;
if (typeof rule.schema.default === "function") {
Expand Down Expand Up @@ -504,11 +515,12 @@ class Validator {
schema.optional = true;

// Check 'nullable' flag
const isNullable = schema.rules
const nullCheck = this.opts.considerNullAsAValue ? false : true;
const setNullable = schema.rules
.map(s => this.getRuleFromSchema(s))
.every(rule => rule.schema.nullable === true);
if (isNullable)
schema.nullable = true;
.every(rule => rule.schema.nullable === nullCheck);
if (setNullable)
schema.nullable = nullCheck;
}

if (schema.$$type) {
Expand Down
247 changes: 195 additions & 52 deletions test/integration.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1109,75 +1109,218 @@ describe("Test optional option", () => {
});

describe("Test nullable option", () => {
const v = new Validator();
describe("old case", () => {
const v = new Validator();

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

expect(check(check)).toBeInstanceOf(Array);
expect(check({ foo: undefined })).toBeInstanceOf(Array);
});
expect(check(check)).toBeInstanceOf(Array);
expect(check({ foo: undefined })).toBeInstanceOf(Array);
});

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

const o = { foo: null };
expect(check(o)).toBe(true);
expect(o.foo).toBe(null);
});
const o = { foo: null };
expect(check(o)).toBe(true);
expect(o.foo).toBe(null);
});

it("should not throw error if value exist", () => {
const schema = { foo: { type: "number", nullable: true } };
const check = v.compile(schema);
expect(check({ foo: 2 })).toBe(true);
});
it("should not throw error if value exist", () => {
const schema = { foo: { type: "number", nullable: true } };
const check = v.compile(schema);
expect(check({ foo: 2 })).toBe(true);
});

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

const o1 = { foo: 2 };
expect(check(o1)).toBe(true);
expect(o1.foo).toBe(2);
const o1 = { foo: 2 };
expect(check(o1)).toBe(true);
expect(o1.foo).toBe(2);

const o2 = {};
expect(check(o2)).toBe(true);
expect(o2.foo).toBe(5);
});
const o2 = {};
expect(check(o2)).toBe(true);
expect(o2.foo).toBe(5);
});

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

const o = { foo: null };
expect(check(o)).toBe(true);
expect(o.foo).toBe(null);
});
const o = { foo: null };
expect(check(o)).toBe(true);
expect(o.foo).toBe(null);
});

it("should work with optional", () => {
const schema = { foo: { type: "number", nullable: true, optional: true } };
const check = v.compile(schema);
it("should work with optional", () => {
const schema = { foo: { type: "number", nullable: true, optional: true } };
const check = v.compile(schema);

expect(check({ foo: 3 })).toBe(true);
expect(check({ foo: null })).toBe(true);
expect(check({})).toBe(true);
expect(check({ foo: 3 })).toBe(true);
expect(check({ foo: null })).toBe(true);
expect(check({})).toBe(true);
});

it("should work with optional and default", () => {
const schema = { foo: { type: "number", nullable: true, optional: true, default: 5 } };
const check = v.compile(schema);

expect(check({ foo: 3 })).toBe(true);

const o1 = { foo: null };
expect(check(o1)).toBe(true);
expect(o1.foo).toBe(null);

const o2 = {};
expect(check(o2)).toBe(true);
expect(o2.foo).toBe(5);
});

it("should accept null value when optional", () => {
const schema = { foo: { type: "number", nullable: false, optional: true } };
const check = v.compile(schema);

expect(check({ foo: 3 })).toBe(true);
expect(check({ foo: undefined })).toBe(true);
expect(check({})).toBe(true);
expect(check({ foo: null })).toBe(true);
});

it("should accept null as value when required", () => {
const schema = {foo: {type: "number", nullable: true, optional: false}};
const check = v.compile(schema);

expect(check({ foo: 3 })).toBe(true);
expect(check({ foo: undefined })).toEqual([{"actual": undefined, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]);
expect(check({})).toEqual([{"actual": undefined, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]);
expect(check({ foo: null })).toBe(true);
});

it("should not accept null as value when required and not explicitly not nullable", () => {
const schema = {foo: {type: "number", optional: false}};
const check = v.compile(schema);

expect(check({ foo: 3 })).toBe(true);
expect(check({ foo: undefined })).toEqual([{"actual": undefined, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]);
expect(check({})).toEqual([{"actual": undefined, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]);
expect(check({ foo: null })).toEqual([{"actual": null, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]);
});
});

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

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

const o1 = { foo: null };
expect(check(o1)).toBe(true);
expect(o1.foo).toBe(null);
expect(check(check)).toBeInstanceOf(Array);
expect(check({ foo: undefined })).toBeInstanceOf(Array);
});

const o2 = {};
expect(check(o2)).toBe(true);
expect(o2.foo).toBe(5);
it("should not throw error if value is null", () => {
const schema = { foo: { type: "number" } };
const check = v.compile(schema);

const o = { foo: null };
expect(check(o)).toBe(true);
expect(o.foo).toBe(null);
});

it("should not throw error if value exist", () => {
const schema = { foo: { type: "number" } };
const check = v.compile(schema);
expect(check({ foo: 2 })).toBe(true);
});

it("should set default value if there is a default", () => {
const schema = { foo: { type: "number", default: 5 } };
const check = v.compile(schema);

const o1 = { foo: 2 };
expect(check(o1)).toBe(true);
expect(o1.foo).toBe(2);

const o2 = {};
expect(check(o2)).toBe(true);
expect(o2.foo).toBe(5);
});

it("should not set default value if current value is null", () => {
const schema = { foo: { type: "number", default: 5 } };
const check = v.compile(schema);

const o = { foo: null };
expect(check(o)).toBe(true);
expect(o.foo).toBe(null);
});

it("should set default value if current value is null but can't be", () => {
const schema = { foo: { type: "number", default: 5, nullable: false } };
const check = v.compile(schema);

const o = { foo: null };
expect(check(o)).toBe(true);
expect(o.foo).toBe(5);
});

it("should set default value if current value is null but optional", () => {
const schema = { foo: { type: "number", default: 5, nullable: false, optional: true } };
const check = v.compile(schema);

const o = { foo: null };
expect(check(o)).toBe(true);
expect(o.foo).toBe(5);
});

it("should work with optional", () => {
const schema = { foo: { type: "number", optional: true } };
const check = v.compile(schema);

expect(check({ foo: 3 })).toBe(true);
expect(check({ foo: null })).toBe(true);
expect(check({})).toBe(true);
});

it("should work with optional and default", () => {
const schema = { foo: { type: "number", optional: true, default: 5 } };
const check = v.compile(schema);

expect(check({ foo: 3 })).toBe(true);

const o1 = { foo: null };
expect(check(o1)).toBe(true);
expect(o1.foo).toBe(null);

const o2 = {};
expect(check(o2)).toBe(true);
expect(o2.foo).toBe(5);
});

it("should not accept null value even if optional", () => {
const schema = { foo: { type: "number", nullable: false, optional: true } };
const check = v.compile(schema);

expect(check({ foo: 3 })).toBe(true);
expect(check({ foo: undefined })).toBe(true);
expect(check({})).toBe(true);
expect(check({ foo: null })).toEqual([{"actual": null, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]);
});

it("should not accept null as value", () => {
const schema = {foo: {type: "number", nullable: false}};
const check = v.compile(schema);

expect(check({ foo: 3 })).toBe(true);
expect(check({ foo: undefined })).toEqual([{"actual": undefined, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]);
expect(check({})).toEqual([{"actual": undefined, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]);
expect(check({ foo: null })).toEqual([{"actual": null, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]);
});
});
});

Expand Down
Loading

0 comments on commit 5fe01c3

Please sign in to comment.