Mankind1023
Mankind1023

Reputation: 7732

Javascript - elegant way to check object has required properties

I'm looking for an good / elegant way to validate that a javascript object has the required properties, so far this is what I have:

var fields = ['name','age','address'];
var info = {
  name: "John Doe",
  age: "",
  phone: "123-456-7890"
}


var validateFields = function(o, required_fields) {
  required_fields.forEach(function(field){
    if(o.hasOwnProperty(field)){
      if(o[field]){
        console.log(field + ": " + o[field]);
      }else{
        console.log(field + " exists but is empty");
      }
    }else{
      console.log(field + " doesn't exist in object");
    }
  });
}

validateFields(info, fields);

Is there a more efficient / elegant way of doing this in plain javascript?

EDIT: Ok so I'm glad I asked because I had completely missed a bunch of possible conditions like zero.

With elegance out the window, how is this for a validation function? Are there any other cases I should be checking for?

var fields = ['name','age','address'];
var info = {
  name: "John Doe",
  age: 0,
  address: false,
  phone: "123-456-7890"
}


var validateFields = function(o, required_fields, null_valid, zero_valid, empty_valid) {
  var invalid_fields = [];
  required_fields.forEach(function(field){
    if(field in o){
      switch(o[field]){
        case '':
          console.log(field + " exists but is empty");
          if(!empty_valid){
            invalid_fields.push(o[field]);
          }
          break;
        case undefined:
          console.log(field + " exists but is undefined");
          invalid_fields.push(o[field]);
          break;
        case null:
          console.log(field + " exists but is null");
          if(!null_valid){
            invalid_fields.push(o[field]);
          }
          break;
        case 0:
          console.log(field + " exists and the value is 0");
          if(!zero_valid){
          }
          invalid_fields.push(o[field]);
          break;
        default:
          console.log(field + ": " + o[field]);
          break;
      }
    }else{
      console.log(field + " doesn't exist in object");
      invalid_fields.push(o[field]);
    }
  });

  return invalid_fields;
}


var invalid = validateFields(info, fields, true, true, false);
console.log(invalid);
if(invalid.length >0){
  console.log("ERROR: Missing fields");
}else{
  console.log("Fields are valid");
}

Upvotes: 22

Views: 46668

Answers (7)

user1889017
user1889017

Reputation:

!You can do the following

const checkObj = (obj, fields) => {
    if ('object' !== typeof obj || null == obj) {
      console.log('Object is not valid');
      return false;
    }
    const hasOnlyTheKeys = JSON.stringify(Object.keys(obj).sort()) === JSON.stringify(fields.sort());

    if (false === hasOnlyTheKeys) {
      console.log('Keys do not match');
      return false;
    }
  
    for (let prop in Obj) {
      switch (obj[prop]) {
        case null:
        case undefined:
          console.log(prop + ' is undefined');
          break;
        case '':
          console.log(prop + ' is empty string');
          break;
        case 0:
          console.log(prop + ' is 0');
          break;
        default:
      }
    }
  };

You can validate the fields array if you please Array.isArray(fields)

Upvotes: 0

Nevada
Nevada

Reputation: 173

Usage

exempleUsage(username: string, password: string) {
  checkRequiredProperties({username, password})
  return this.apiService.post(`/.../...`)
}

Helpers

const isEmpty = (v) => (!v && v !== false && v !== 0);

const handleErrorForCB = (error, errorCB) => {
  if (!errorCB) throw error;
  errorCB(error);
};

export function hasProperty(o, p) {
  return !isEmpty(_.get(o, p));
}

export function checkRequiredProperties(o, properties = Object.keys(o), errorCB) {
  const errors = properties.filter(p => !hasProperty(o, p));
  if (errors.length > 0) handleErrorForCB(new Error("Missing properties: " + errors), errorCB);
}

export function checkFoundProperties(o, properties = Object.keys(o), errorCB) {
  const errors = properties.filter(p => !hasProperty(o, p));
  if (errors.length > 0) handleErrorForCB(new Error("Not found: " + errors), errorCB);
}

Upvotes: 0

Jacob Rex
Jacob Rex

Reputation: 41

Here's a function that will check that an Object contains every required key by comparing its keys to an array of strings. Since it validates by comparing lengths, it won't matter if the order is different or if the object contains additional information.

const hasRequiredKeys = (object, requiredKeys) => {
  return Object.keys(object).filter(key => requiredKeys.includes(key)).length === requiredKeys.length
}

Upvotes: 0

Jamie McCrindle
Jamie McCrindle

Reputation: 9214

You can use https://github.com/jquense/yup to solve your problem.

import * as yup from 'yup';

let schema = yup.object().shape({
  name: yup.string().required(),
  age: yup.number().required().positive().integer(),
  address: yup.string().required(),
});

var info = {
  name: "John Doe",
  age: "",
  phone: "123-456-7890"
}

// check validity
schema
  .isValid(info)
  .then(function (valid) {
    valid; // => false
  });

This is doing a bit more than just checking for the existence of fields, it's also ensuring that the data type is correct e.g. that age is a non-negative number.

Upvotes: 13

Patrick Roberts
Patrick Roberts

Reputation: 51916

If you want "elegant", what you're looking for is called a schema:

var schema = {
  name: function (value) {
    return /^([A-Z][a-z\-]* )+[A-Z][a-z\-]*( \w+\.?)?$/.test(value);
  },
  age: function (value) {
    return !isNaN(value) && parseInt(value) == value && value >= 18;
  },
  phone: function (value) {
    return /^(\+?\d{1,2}-)?\d{3}-\d{3}-\d{4}$/.test(value);
  }
};

var info = {
  name: "John Doe",
  age: "",
  phone: "123-456-7890"
};

function validate(object, schema) {
  var errors = Object.keys(schema).filter(function (key) {
    return !schema[key](object[key]);
  }).map(function (key) {
    return new Error(key + " is invalid.");
  });

  if (errors.length > 0) {
    errors.forEach(function (error) {
      console.log(error.message);
    });
  } else {
    console.log("info is valid");
  }
}

validate(info, schema);

To address @AndreFigueiredo's pedantry, you can also check if the object contains the property at all:

var schema = {
  name: function (value) {
    return /^([A-Z][a-z\-]* )+[A-Z][a-z\-]*( \w+\.?)?$/.test(value);
  },
  age: function (value) {
    return !isNaN(value) && parseInt(value) == value && value >= 18;
  },
  phone: function (value) {
    return /^(\+?\d{1,2}-)?\d{3}-\d{3}-\d{4}$/.test(value);
  }
};

schema.name.required = true;
schema.age.required = true;

var info = {
  // name: "John Doe",
  age: "",
  phone: "123-456-7890"
};

function validate(object, schema) {
  var errors = Object.keys(schema).map(function (property) {
    var validator = schema[property];
    
    return [property, !validator.required || (property in object), validator(object[property])];
  }).filter(function (entry) {
    return !entry[1] || !entry[2];
  }).map(function (entry) {
    if (!entry[1]) return new Error(entry[0] + " is required.");
    else return new Error(entry[0] + " is invalid.");
  });

  if (errors.length > 0) {
    errors.forEach(function (error) {
      console.log(error.message);
    });
  } else {
    console.log("info is valid");
  }
}

validate(info, schema);


Update

Here's a modernized solution using features from ECMAScript 6 edition including destructuring, arrow functions, Object.entries(), template literals, and for...of:

const schema = {
  name: value => /^([A-Z][a-z\-]* )+[A-Z][a-z\-]*( \w+\.?)?$/.test(value),
  age: value => parseInt(value) === Number(value) && value >= 18,
  phone: value => /^(\+?\d{1,2}-)?\d{3}-\d{3}-\d{4}$/.test(value)
};

let info = {
  name: 'John Doe',
  age: '',
  phone: '123-456-7890'
};

const validate = (object, schema) => Object
  .keys(schema)
  .filter(key => !schema[key](object[key]))
  .map(key => new Error(`${key} is invalid.`));

const errors = validate(info, schema);

if (errors.length > 0) {
  for (const { message } of errors) {
    console.log(message);
  }
} else {
  console.log('info is valid');
}

And the version that performs required and validate checks separately:

const schema = {
  name: value => /^([A-Z][a-z\-]* )+[A-Z][a-z\-]*( \w+\.?)?$/.test(value),
  age: value => parseInt(value) === Number(value) && value >= 18,
  phone: value => /^(\+?\d{1,2}-)?\d{3}-\d{3}-\d{4}$/.test(value)
};

schema.name.required = true;
schema.age.required = true;

let info = {
  // name: 'John Doe',
  age: '',
  phone: '123-456-7890'
};

const validate = (object, schema) => Object
  .entries(schema)
  .map(([key, validate]) => [
    key,
    !validate.required || (key in object),
    validate(object[key])
  ])
  .filter(([_, ...tests]) => !tests.every(Boolean))
  .map(([key, invalid]) => new Error(`${key} is ${invalid ? 'invalid' : 'required'}.`));

const errors = validate(info, schema);

if (errors.length > 0) {
  for (const { message } of errors) {
    console.log(message);
  }
} else {
  console.log('info is valid');
}

Upvotes: 45

Nina Scholz
Nina Scholz

Reputation: 386680

I would do for a real check for empty strings, because 0 is falsy, but a value and not empty.

function validateFields(object, keys) {
    keys.forEach(function (k) {
        if (k in object) {
            console.log(k + ": " + object[k]);
            if (object[k] === '') {
                console.log(k + " exists but is empty");
            }
            return;
        }
        console.log(k + " doesn't exist in object");
    });
}

var fields = ['name', 'age', 'address', 'zeroString', 'zeroNumber'],
    info = { name: "John Doe", age: "", phone: "123-456-7890", zeroString: '0', zeroNumber: 0 };

validateFields(info, fields);

Upvotes: 1

Vi100
Vi100

Reputation: 4203

I think that you should be more concerned about what's the proper way of checking for it, rather than the most elegant. I'll drop a link to a very good post from Todd Moto regarding this: Please, take a look

In short your code should look like this:

 var validateFields = function(o, required_fields) {
  required_fields.forEach(function(field){
    if(field in o){
      if((typeof o[field] != 'undefined')){
        console.log(field + ": " + o[field]);
      }else{
        console.log(field + " exists but is undefined");
      }
    }else{
      console.log(field + " doesn't exist in object");
    }
  });
}

Note: Take care when checking it has value, many expressions in javasscript are falsy (eg. 0, false, etc.) but are valid values.

Upvotes: 2

Related Questions