function init(self, obj, doc, opts, prefix) {
  prefix = prefix || '';

  if (obj.$__ != null) {
    obj = obj._doc;
  }
  const keys = Object.keys(obj);
  const len = keys.length;
  let schemaType;
  let path;
  let i;
  const strict = self.$__.strictMode;
  const docSchema = self.$__schema;

  for (let index = 0; index < len; ++index) {
    i = keys[index];
    // avoid prototype pollution
    if (specialProperties.has(i)) {
      continue;
    }
    path = prefix ? prefix + i : i;
    schemaType = docSchema.path(path);
    // Should still work if not a model-level discriminator, but should not be
    // necessary. This is *only* to catch the case where we queried using the
    // base model and the discriminated model has a projection
    if (docSchema.$isRootDiscriminator && !self.$__isSelected(path)) {
      continue;
    }

    const value = obj[i];
    if (!schemaType && utils.isPOJO(value)) {
      // assume nested object
      if (!doc[i]) {
        doc[i] = {};
        if (!strict && !(i in docSchema.tree) && !(i in docSchema.methods) && !(i in docSchema.virtuals)) {
          self[i] = doc[i];
        }
      }
      init(self, value, doc[i], opts, path + '.');
    } else if (!schemaType) {
      doc[i] = value;
      if (!strict && !prefix) {
        self[i] = value;
      }
    } else {
      // Retain order when overwriting defaults
      if (doc.hasOwnProperty(i) && value !== void 0 && !opts.hydratedPopulatedDocs) {
        delete doc[i];
      }
      if (value === null) {
        doc[i] = schemaType._castNullish(null);
      } else if (value !== undefined) {
        const wasPopulated = value.$__ == null ? null : value.$__.wasPopulated;

        if (schemaType && !wasPopulated && !opts.hydratedPopulatedDocs) {
          try {
            if (opts && opts.setters) {
              // Call applySetters with `init = false` because otherwise setters are a noop
              const overrideInit = false;
              doc[i] = schemaType.applySetters(value, self, overrideInit);
            } else {
              doc[i] = schemaType.cast(value, self, true);
            }
          } catch (e) {
            self.invalidate(e.path, new ValidatorError({
              path: e.path,
              message: e.message,
              type: 'cast',
              value: e.value,
              reason: e
            }));
          }
        } else if (schemaType && opts.hydratedPopulatedDocs) {
          doc[i] = schemaType.cast(value, self, true, undefined, { hydratedPopulatedDocs: true });

          if (doc[i] && doc[i].$__ && doc[i].$__.wasPopulated) {
            self.$populated(path, doc[i].$__.wasPopulated.value, doc[i].$__.wasPopulated.options);
          } else if (Array.isArray(doc[i]) && doc[i].length && doc[i][0]?.$__?.wasPopulated) {
            self.$populated(path, doc[i].map(populatedDoc => populatedDoc?.$__?.wasPopulated?.value).filter(val => val != null), doc[i][0].$__.wasPopulated.options);
          }
        } else {
          doc[i] = value;
        }
      }
      // mark as hydrated
      if (!self.$isModified(path)) {
        self.$__.activePaths.init(path);
      }
    }
  }
}
Init helper.

@param {Object} self document instance
@param {Object} obj raw mongodb doc
@param {Object} doc object we are initializing
@param {Object} [opts] Optional Options
@param {Boolean} [opts.setters] Call `applySetters` instead of `cast`
@param {String} [prefix] Prefix to add to each path
@api private