Anders Östman Anders Östman - 2 months ago 7
Node.js Question

Writing JSON validation in NodeJs from scratch

I have been using Joi validation for a while now, for most situations it is pretty solid, but every time I need to do something custom it require a bit more work. As a learning experiment i have been thinking of writing my own lightweight JSON validation "boilerplate".

The premises:


  • This validation is server only (NodeJs), which means I can use modern javascript functions/methods.

  • I'm using a JSON body parser so i know that POST data is valid JSON, that means i dont have to care about certain values like NaN och undefined.



Lets consider this small piece of JSON coming from a POST:

{
"name": "Anders",
"dur": 3600000,
"staff": [
"Eric",
"John",
],
"unwantedProp": "foo"
}


I want to


  • Make sure the following properties exist:
    name
    ,
    dur
    , and
    staff
    . Ignore all other.

  • Make sure
    name
    is string.

  • Make sure
    dur
    is number. (Or convert if string).

  • Make sure
    staff
    is array, and only contains strings.

  • Create a bad request error directly when something is wrong.



To get started i wrote this piece of code and put it in a module.

var error = function (message, status) {
var e = new Error(message);
e.status = status;
return e;
};

module.exports.validateClient = function(json, callback) {

var result = {};

if (!json.hasOwnProperty('name')) {
return callback(error('name is not defined', 400));
} else if (typeof json.name === 'string') {
result.name = json.name
} else {
return callback(error('name must be a string', 400));
}


if (!json.hasOwnProperty('dur')) {
return callback(error('dur is not defined', 400));
} else if (typeof json.dur === 'number') {
result.dur = Math.floor( json.dur )
} else if (typeof json.dur === 'string') {
result.dur = parseInt(json.dur, 10);
} else {
return callback(error('dur must be a number', 400));
}

if (!json.hasOwnProperty('staff')) {
return callback(error('staff is not defined', 400));
} else if (Array.isArray(json.staff)) {
var valid = true
json.staff.forEach(function (elem) {
if (typeof elem !== 'string') {
valid = false;
}
})
if (valid) {
result.staff = json.staff;
} else {
return callback(error('staff may only contain strings', 400));
}
} else {
return callback(error('staff must be an array', 400));
}

callback(null, result);
};


I then use this function like:

hlpr.validateClient(req.body, function(err, result) {
if (err) return next(err);
res.json(result);
})


Even though this works, I know it's a total mess of if statements... and its totaly non reusable.
This brings me to my somewhat vague question. Can someone please give me some guidelines of how to break down this mess into smaller reusable functions that can be applied to other JSON data... Maybe help me with an example of a modularized part of my if-statement mess.

Answer

You should use some kind of schema to validate your stuff.

A very flexible way of doing this is to allow for custom validation functions for some of the properties.

Here is an example:

var source ={
  "name": "Anders",
  "dur": 3600000,
  "staff": [
    "Eric",
    "John",
  ],
  "unwantedProp": "foo"
}

var schema = {
  "name": function(name){return typeof name ==="string"&&name.length<10},
  "dur": "number",
  "staff": ["string"],
}

function validate(source, schema, result){
    result = result||{}
    var error
    Object.keys(schema).forEach(function(key){
        var val = source[key]
        var type = schema[key]
        if(type === "string"){
            if(typeof val ==="string"){
                result[key] = val
            }else{
                error = {success:false, message:val+" is not a string."}
            }
        }else if(type === "number"){
            if(typeof val ==="number"){
                result[key] = val
            }else if (!isNaN(parseInt(val))){
                result[key] = parseInt(val)
            }else{
                error = {success:false, message:val+" is not a number."}
            }
        }else if (type.constructor === Array){
            if(val.constructor === Array){
                var arr_type = type[0]
                var valid = true
                val.forEach(function(mem){
                    if(typeof mem !==arr_type){valid = false}
                })
                if(valid){
                    result[key] = val
                }else{
                    error = {success:false, message:val+" is not a "+ arr_type+" array."}
                }
            }else{
                error = {success:false, message:val+" is not an array."}
            }
        }else if (typeof type === "function"){
            if(type(val)){
                 result[key] = val
            }else{
                error = {success:false, message:"Custom validation "+type+ " returned false for "+val+"."}
            }
        }
    })
    return error||{sucess:true, result:result}
}