function walkUpdatePath(schema, obj, op, options, context, filter, pref) {
  const strict = options.strict;
  const prefix = pref ? pref + '.' : '';
  const keys = Object.keys(obj);
  let i = keys.length;
  let hasKeys = false;
  let schematype;
  let key;
  let val;

  let aggregatedError = null;

  const strictMode = strict != null ? strict : schema.options.strict;

  while (i--) {
    key = keys[i];
    val = obj[key];

    // `$pull` is special because we need to cast the RHS as a query, not as
    // an update.
    if (op === '$pull') {
      schematype = schema._getSchema(prefix + key);
      if (schematype == null) {
        const _res = getEmbeddedDiscriminatorPath(schema, obj, filter, prefix + key, options);
        if (_res.schematype != null) {
          schematype = _res.schematype;
        }
      }
      if (schematype != null && schematype.schema != null) {
        obj[key] = cast(schematype.schema, obj[key], options, context);
        hasKeys = true;
        continue;
      }
    }

    const discriminatorKey = (prefix ? prefix + key : key);
    if (
      schema.discriminatorMapping != null &&
      discriminatorKey === schema.options.discriminatorKey &&
      schema.discriminatorMapping.value !== obj[key] &&
      !options.overwriteDiscriminatorKey
    ) {
      if (strictMode === 'throw') {
        const err = new Error('Can\'t modify discriminator key "' + discriminatorKey + '" on discriminator model');
        aggregatedError = _appendError(err, context, discriminatorKey, aggregatedError);
        continue;
      } else if (strictMode) {
        delete obj[key];
        continue;
      }
    }

    if (getConstructorName(val) === 'Object') {
      // watch for embedded doc schemas
      schematype = schema._getSchema(prefix + key);

      if (schematype == null) {
        const _res = getEmbeddedDiscriminatorPath(schema, obj, filter, prefix + key, options);
        if (_res.schematype != null) {
          schematype = _res.schematype;
        }
      }

      if (op !== '$setOnInsert' &&
          handleImmutable(schematype, strict, obj, key, prefix + key, options, context)) {
        continue;
      }

      if (schematype && schematype.caster && op in castOps) {
        // embedded doc schema
        if ('$each' in val) {
          hasKeys = true;
          try {
            obj[key] = {
              $each: castUpdateVal(schematype, val.$each, op, key, context, prefix + key)
            };
          } catch (error) {
            aggregatedError = _appendError(error, context, key, aggregatedError);
          }

          if (val.$slice != null) {
            obj[key].$slice = val.$slice | 0;
          }

          if (val.$sort) {
            obj[key].$sort = val.$sort;
          }

          if (val.$position != null) {
            obj[key].$position = castNumber(val.$position);
          }
        } else {
          if (schematype != null && schematype.$isSingleNested) {
            const _strict = strict == null ? schematype.schema.options.strict : strict;
            try {
              obj[key] = schematype.castForQuery(null, val, context, { strict: _strict });
            } catch (error) {
              aggregatedError = _appendError(error, context, key, aggregatedError);
            }
          } else {
            try {
              obj[key] = castUpdateVal(schematype, val, op, key, context, prefix + key);
            } catch (error) {
              aggregatedError = _appendError(error, context, key, aggregatedError);
            }
          }

          if (obj[key] === void 0) {
            delete obj[key];
            continue;
          }

          hasKeys = true;
        }
      } else if ((op === '$currentDate') || (op in castOps && schematype)) {
        // $currentDate can take an object
        try {
          obj[key] = castUpdateVal(schematype, val, op, key, context, prefix + key);
        } catch (error) {
          aggregatedError = _appendError(error, context, key, aggregatedError);
        }

        if (obj[key] === void 0) {
          delete obj[key];
          continue;
        }

        hasKeys = true;
      } else if (op === '$rename') {
        const schematype = new SchemaString(`${prefix}${key}.$rename`);
        try {
          obj[key] = castUpdateVal(schematype, val, op, key, context, prefix + key);
        } catch (error) {
          aggregatedError = _appendError(error, context, key, aggregatedError);
        }

        if (obj[key] === void 0) {
          delete obj[key];
          continue;
        }

        hasKeys = true;
      } else {
        const pathToCheck = (prefix + key);
        const v = schema._getPathType(pathToCheck);
        let _strict = strict;
        if (v && v.schema && _strict == null) {
          _strict = v.schema.options.strict;
        }

        if (v.pathType === 'undefined') {
          if (_strict === 'throw') {
            throw new StrictModeError(pathToCheck);
          } else if (_strict) {
            delete obj[key];
            continue;
          }
        }

        // gh-2314
        // we should be able to set a schema-less field
        // to an empty object literal
        hasKeys |= walkUpdatePath(schema, val, op, options, context, filter, prefix + key) ||
          (utils.isObject(val) && Object.keys(val).length === 0);
      }
    } else {
      const checkPath = (key === '$each' || key === '$or' || key === '$and' || key === '$in') ?
        pref : prefix + key;
      schematype = schema._getSchema(checkPath);

      // You can use `$setOnInsert` with immutable keys
      if (op !== '$setOnInsert' &&
          handleImmutable(schematype, strict, obj, key, prefix + key, options, context)) {
        continue;
      }

      let pathDetails = schema._getPathType(checkPath);

      // If no schema type, check for embedded discriminators because the
      // filter or update may imply an embedded discriminator type. See #8378
      if (schematype == null) {
        const _res = getEmbeddedDiscriminatorPath(schema, obj, filter, checkPath, options);
        if (_res.schematype != null) {
          schematype = _res.schematype;
          pathDetails = _res.type;
        }
      }

      let isStrict = strict;
      if (pathDetails && pathDetails.schema && strict == null) {
        isStrict = pathDetails.schema.options.strict;
      }

      const skip = isStrict &&
        !schematype &&
        !/real|nested/.test(pathDetails.pathType);

      if (skip) {
        // Even if strict is `throw`, avoid throwing an error because of
        // virtuals because of #6731
        if (isStrict === 'throw' && schema.virtuals[checkPath] == null) {
          throw new StrictModeError(prefix + key);
        } else {
          delete obj[key];
        }
      } else {
        if (op === '$rename') {
          if (obj[key] == null) {
            throw new CastError('String', obj[key], `${prefix}${key}.$rename`);
          }
          const schematype = new SchemaString(`${prefix}${key}.$rename`);
          obj[key] = schematype.castForQuery(null, obj[key], context);
          continue;
        }

        try {
          if (prefix.length === 0 || key.indexOf('.') === -1) {
            obj[key] = castUpdateVal(schematype, val, op, key, context, prefix + key);
          } else if (isStrict !== false || schematype != null) {
            // Setting a nested dotted path that's in the schema. We don't allow paths with '.' in
            // a schema, so replace the dotted path with a nested object to avoid ending up with
            // dotted properties in the updated object. See (gh-10200)
            setDottedPath(obj, key, castUpdateVal(schematype, val, op, key, context, prefix + key));
            delete obj[key];
          }
        } catch (error) {
          aggregatedError = _appendError(error, context, key, aggregatedError);
        }

        if (Array.isArray(obj[key]) && (op === '$addToSet' || op === '$push') && key !== '$each') {
          if (schematype &&
              schematype.caster &&
              !schematype.caster.$isMongooseArray &&
              !schematype.caster[schemaMixedSymbol]) {
            obj[key] = { $each: obj[key] };
          }
        }

        if (obj[key] === void 0) {
          delete obj[key];
          continue;
        }

        hasKeys = true;
      }
    }
  }

  if (aggregatedError != null) {
    throw aggregatedError;
  }

  return hasKeys;
}
Walk each path of obj and cast its values
according to its schema.

@param {Schema} schema
@param {Object} obj part of a query
@param {String} op the atomic operator ($pull, $set, etc)
@param {Object} [options]
@param {Boolean|String} [options.strict]
@param {Query} context
@param {Object} filter
@param {String} pref path prefix (internal only)
@return {Bool} true if this path has keys to update
@api private