function crawl(objIn, objOut, schema, list, base, path) {
    path = path || [];

    var keys = Object.keys(objIn);

    for(var i = 0; i < keys.length; i++) {
        var k = keys[i];

        var p = path.slice();
        p.push(k);

        var valIn = objIn[k];
        var valOut = objOut[k];

        var nestedSchema = getNestedSchema(schema, k);
        var nestedValType = (nestedSchema || {}).valType;
        var isInfoArray = nestedValType === 'info_array';
        var isColorscale = nestedValType === 'colorscale';
        var items = (nestedSchema || {}).items;

        if(!isInSchema(schema, k)) {
            list.push(format('schema', base, p));
        } else if(isPlainObject(valIn) && isPlainObject(valOut) && nestedValType !== 'any') {
            crawl(valIn, valOut, nestedSchema, list, base, p);
        } else if(isInfoArray && isArray(valIn)) {
            if(valIn.length > valOut.length) {
                list.push(format('unused', base, p.concat(valOut.length)));
            }
            var len = valOut.length;
            var arrayItems = Array.isArray(items);
            if(arrayItems) len = Math.min(len, items.length);
            var m, n, item, valInPart, valOutPart;
            if(nestedSchema.dimensions === 2) {
                for(n = 0; n < len; n++) {
                    if(isArray(valIn[n])) {
                        if(valIn[n].length > valOut[n].length) {
                            list.push(format('unused', base, p.concat(n, valOut[n].length)));
                        }
                        var len2 = valOut[n].length;
                        for(m = 0; m < (arrayItems ? Math.min(len2, items[n].length) : len2); m++) {
                            item = arrayItems ? items[n][m] : items;
                            valInPart = valIn[n][m];
                            valOutPart = valOut[n][m];
                            if(!Lib.validate(valInPart, item)) {
                                list.push(format('value', base, p.concat(n, m), valInPart));
                            } else if(valOutPart !== valInPart && valOutPart !== +valInPart) {
                                list.push(format('dynamic', base, p.concat(n, m), valInPart, valOutPart));
                            }
                        }
                    } else {
                        list.push(format('array', base, p.concat(n), valIn[n]));
                    }
                }
            } else {
                for(n = 0; n < len; n++) {
                    item = arrayItems ? items[n] : items;
                    valInPart = valIn[n];
                    valOutPart = valOut[n];
                    if(!Lib.validate(valInPart, item)) {
                        list.push(format('value', base, p.concat(n), valInPart));
                    } else if(valOutPart !== valInPart && valOutPart !== +valInPart) {
                        list.push(format('dynamic', base, p.concat(n), valInPart, valOutPart));
                    }
                }
            }
        } else if(nestedSchema.items && !isInfoArray && isArray(valIn)) {
            var _nestedSchema = items[Object.keys(items)[0]];
            var indexList = [];

            var j, _p;

            // loop over valOut items while keeping track of their
            // corresponding input container index (given by _index)
            for(j = 0; j < valOut.length; j++) {
                var _index = valOut[j]._index || j;

                _p = p.slice();
                _p.push(_index);

                if(isPlainObject(valIn[_index]) && isPlainObject(valOut[j])) {
                    indexList.push(_index);
                    var valInj = valIn[_index];
                    var valOutj = valOut[j];
                    if(isPlainObject(valInj) && valInj.visible !== false && valOutj.visible === false) {
                        list.push(format('invisible', base, _p));
                    } else crawl(valInj, valOutj, _nestedSchema, list, base, _p);
                }
            }

            // loop over valIn to determine where it went wrong for some items
            for(j = 0; j < valIn.length; j++) {
                _p = p.slice();
                _p.push(j);

                if(!isPlainObject(valIn[j])) {
                    list.push(format('object', base, _p, valIn[j]));
                } else if(indexList.indexOf(j) === -1) {
                    list.push(format('unused', base, _p));
                }
            }
        } else if(!isPlainObject(valIn) && isPlainObject(valOut)) {
            list.push(format('object', base, p, valIn));
        } else if(!isArrayOrTypedArray(valIn) && isArrayOrTypedArray(valOut) && !isInfoArray && !isColorscale) {
            list.push(format('array', base, p, valIn));
        } else if(!(k in objOut)) {
            list.push(format('unused', base, p, valIn));
        } else if(!Lib.validate(valIn, nestedSchema)) {
            list.push(format('value', base, p, valIn));
        } else if(nestedSchema.valType === 'enumerated' &&
            (
                (nestedSchema.coerceNumber && valIn !== +valOut) ||
                (!isArrayOrTypedArray(valIn) && valIn !== valOut) ||
                (String(valIn) !== String(valOut))
            )
        ) {
            list.push(format('dynamic', base, p, valIn, valOut));
        }
    }

    return list;
}
Validate a data array and layout object.

@param {array} data
@param {object} layout

@return {array} array of error objects each containing:
 - {string} code
     error code ('object', 'array', 'schema', 'unused', 'invisible' or 'value')
 - {string} container
     container where the error occurs ('data' or 'layout')
 - {number} trace
     trace index of the 'data' container where the error occurs
 - {array} path
     nested path to the key that causes the error
 - {string} astr
     attribute string variant of 'path' compatible with Plotly.restyle and
     Plotly.relayout.
 - {string} msg
     error message (shown in console in logger config argument is enable)