docs: function(value, options){
          // NOTE - the nesting is taken care of by validateParams.validate()
          // so the valueConstraints and mapConstraints options don't need to be
          // applied here
          
          // implicitly pass undefined
          if(typeof value === 'undefined') return undefined;
          
          // make sure the default options object exists
          if(typeof this.options !== 'object') this.options = {};
          
          // build up a base config from the pre-defined defaults
          var config = {};
          config.plainObjectOnly = this.options.plainObjectOnly ? true : false;
          config.keyConstraints = validateParams.extendObject({}, this.options.keyConstraints);
          config.rejectUnspecifiedKeys = this.options.rejectUnspecifiedKeys ? true : false;
          config.sizeIs = parseInt(this.options.sizeIs); // NaN means ignore this option
          config.minimumSize = parseInt(this.options.minimumSize); // NaN means ignore this option
          config.maximumSize = parseInt(this.options.maximumSize); // NaN means ignore this option
          config.message = validateParams.extractValidatorMessage(this, options);
          
          // interpret the passed value
          if(typeof options === 'boolean'){
              config.plainObjectOnly = true;
          }else if(validate.isObject(options)){
              if(options.plainObjectOnly) config.plainObjectOnly = true;
              if(validate.isObject(options.keyConstraints)){
                  Object.keys(options.keyConstraints).forEach(function(vn){
                      config.keyConstraints[vn] = options.keyConstraints[vn]; // overides conflicting global validations intentionally
                  });
              }
              if(options.rejectUnspecifiedKeys) config.rejectUnspecifiedKeys = true;
              if(!isNaN(parseInt(options.sizeIs))) config.sizeIs = parseInt(options.sizeIs);
              if(!isNaN(parseInt(options.minimumSize))) config.minimumSize = parseInt(options.minimumSize);
              if(!isNaN(parseInt(options.maximumSize))) config.maximumSize = parseInt(options.maximumSize);
              if(validate.isString(options.message)) config.message = options.message;
          }else{
              throw new Error('invalid options passed - must be true or a plain object');
          }
          
          // flip the size bounds if needed
          if(!(isNaN(config.minimumSize) || isNaN(config.maximumSize))){
              if(config.maximumSize < config.minimumSize){
                  var trueMin = config.maximumSize;
                  var trueMax = config.minimumSize;
                  config.maximumSize = trueMax;
                  config.minimumSize = trueMin;
              }
          }
          
          // implicitly reject non-objects
          if(!validate.isObject(value)){
              return config.message ? config.message : 'must be an object';
          }
          
          var errors = [];
          
          // deal with plainObjectOnly option
          if(config.plainObjectOnly && !validateParams.isPlainObject(value)){
              errors.push('must be a plain object');
          }
          
          // deal with the key restrictions
          Object.keys(value).forEach(function(k){
              var svRes = validate.single(k, config.keyConstraints);
              if(svRes){ // undefined means no error, so a truthy value means an error
                  if(validate.isArray(svRes) && svRes.length === 1){
                      errors.push("key name '" + k + "' " + svRes[0]);
                  }else{
                      errors.push("key name '" + k + "' is invalid");
                  }
              }
          });
          
          // deal with rejectUnspecifiedKeys option
          if(config.rejectUnspecifiedKeys){
              var allowedKeysLookup = {};
              if(validate.isObject(options.mapConstraints)){
                  Object.keys(options.mapConstraints).forEach(function(sk){
                      allowedKeysLookup[sk] = true;
                  });
              }else{
                  validateParams._warn('dictionary validator called with only specified keys allowed but with no keys specified in mapConstraints');
              }
              Object.keys(value).forEach(function(vk){
                  if(!allowedKeysLookup[vk]) errors.push("key '" + vk + "' is not permitted");
              });
          }
          
          // deal with any specified size restrictions
          var numKeys = Object.keys(value).length;
          if(!isNaN(config.sizeIs) && numKeys !== config.sizeIs){
              errors.push('must contain ' + config.sizeIs + ' key-value pairs, but contains ' + numKeys);
          }
          if(!isNaN(config.minimumSize) && numKeys < config.minimumSize){
              errors.push('must contain at least ' + config.minimumSize + ' key-value pairs, but only contains ' + numKeys);
          }
          if(!isNaN(config.maximumSize) && numKeys > config.maximumSize){
              errors.push('may not contain more than ' + config.maximumSize + ' key-value pairs, but contains ' + numKeys);
          }
          
          // retrun as appropraite
          if(errors.length > 0){
              return config.message ? config.message : errors;
          }
          return undefined;
      }