Simple Configuration Validation in Node.js
In Node.js and related Javascript projects, I often find myself making use of configuration objects and message objects parsed from JSON. But relying upon the structure of an object that initially arrived as JSON, whether entered by a user or created by other code, is an open invition for things to go wrong. Thus it is necessary to run at least a few simple checks on these objects when they first appear. Examining the structure of parsed JSON is trivial enough if you are expecting something like:
{ paramOne: "a", paramTwo: 15 }
Did JSON.parse() work, is the result an object, does it have the necessary two parameters and vaguely correct-looking values? Fine, simple to check. But what if you have twenty different expected structures for objects arriving in the form of JSON? That starts to look like a job for a validation system of some sort. Something like my present slowly advancing Node.js side-project may have a dozen different structural components in the codebase, each of which requires a very different set of configuration parameters. Configuration takes the form of JSON from a file, which is parsed into objects to be provided to component class constructors.
The following outlines a trivial form of validation I've been using for my parsed JSON configuration. Firstly each component constructor describes the structure of its own configuration by way of an object. As a simple example, a Redis interface component class might look like this:function RedisInterface(configObj) { RedisInterface.super_.call(this, configObj); }; util.inherits(RedisInterface, DataInterface); var p = RedisInterface.prototype; RedisInterface.CONFIG = { host: { _configInfo: { description: "The hostname of the Redis server.", types: "string", required: true } }, port: { _configInfo: { description: "The port number of the Redis server.", types: "integer", required: true } } };
That RedisInterface.CONFIG object describes a configuration object of the following form:
{ host: "localhost", port: 6379 }
Somewhere near the top of the class hierarchy containing RedisInterface is a Component class whose constructor checks the configObj argument recursively:
function Component(configObj) { this.config = configObj; // If this class has a configuration description associated with its constructor, // then check to see that configuration is correct. if( this.config instanceof Object && this.constructor.CONFIG instanceof Object ) { this._checkConfiguration(this.config, this.constructor.CONFIG); } }; var p = Component.prototype; /* * Recursively step through the configuration and expected configuration objects. * The expected configuration looks like this - but may have multiple levels of * objects. * * { * value1: { * _configInfo: { * description: any string, optional, and not considered by this function * types: array or one of the types returned by this._getType() * required: true or false * allowedValues: array or null * } * }, * value2: ... * } * */ p._checkConfiguration = function(configObj, templateObj, propertyChain) { for( property in templateObj ) { if( propertyChain ) { var thisPropertyChain = propertyChain + "." + property; } else { var thisPropertyChain = property; } var configInfo = templateObj[property]._configInfo; var configValue = configObj[property]; // if this is a nested set of further properties then recurse if( configValue instanceof Object ) { this._checkConfiguration( configValue, templateObj[property], thisPropertyChain ); } // It's quite possible that there is no configInfo for this level of // configuration. If this is the case, then move on to the next property. if( !configInfo ) { continue; } // is this property both required and missing? if( configValue == undefined ) { if( configInfo.required ) { this._configurationError(thisPropertyChain, "required, but missing"); continue; } else { // permitted not to exist, so continue on to the next property continue; } } // check type if( configInfo.types ) { if( !(configInfo.types instanceof Array) ) { configInfo.types = [configInfo.types]; } var found = false; var configValueType = this._getType(configValue); for( var i = 0, l = configInfo.types.length; i < l; i++ ) { if( configValueType == configInfo.types[i] ) { found = true; break; } } if( !found ) { this._configurationError( thisPropertyChain, "invalid type, found " + configValueType + " expecting one of [" + configInfo.types.toString() + "]" ); continue; } } // check allowed values if( configInfo.allowedValues instanceof Array ) { var found = false; for( var i = 0, l = configInfo.allowedValues.length; i < l; i++ ) { if( configInfo.allowedValues[i] == configValue ) { found = true; break; } } if( !found ) { this._configurationError( thisPropertyChain, "invalid value, found " + configValue + " expecting one of [" + configInfo.allowedValues.toString() + "]" ); continue; } } } }; /* * An extended type detection function, adding a couple of useful types to * those already recognized by typeof. * * Returns one of: "object", "array", "boolean", "string", "number", "integer", * "undefined", "null", "function" */ p._getType = function(value) { var type = typeof value; if( type == "object" ) { if( value instanceof Array ) { type = "array"; } else if( value == null ) { type = "null"; } } else if( type == "number" ) { if( value % 1 == 0 ) { type = "integer"; } } else if( type == "string" ) { if( value.match(/^d+$/) ) { type = "integer"; } else if( value.match(/^d*.?d*$/) ) { type = "number"; } } return type; }; /* * This implementation is actually fairly lazy: a better approach would be to * gather up all the errors from this config and push them out all at once at * the end. A still better approach would be to centralize all the configuration * errors and report them all rather than just those from a single class - but * that would be harder to fit into a single blog post as an example. */ p._configurationError = function(propertyChain, error) { throw new Error( "Configuration error for constructor " + this.constructor.name + " and configuration property " + propertyChain + ": " + error ); };
The limitations of this quickly written and simple system should be fairly evident - it is far better than nothing, but it is only checking configuration parameters for type, presence if required, and allowed values if specified. But it should also be clear as to how this could be expanded with little effort to check for, say, correctly formed hostnames, email addresses, telephone numbers, and so forth.