Skip to content

JSON property not updated when its prototype contains default values #6050

@alexandre-abrioux

Description

@alexandre-abrioux

Describe the bug

Hi, and thanks again for maintaining this excellent repo!
I'm back with another bug report regarding JSON properties, sorry in advance 😆

This is a regression that happened somewhere between 5.5.3 and 6.3.10.
After upgrading, I noticed that some JSON properties were not properly persisted in the database.

TLDR: This happens when the JSON object is polluted with a prototype containing keys equal to the actual object keys.

More context

Using the debugger, I found out that the following condition was reached when computing the ChangeSet and comparing values:

if (attrs && attrs.set == null) {
continue;
}

When inspecting the prototype of these JSON properties, I discovered that it was "polluted" with some "default values" copied from their JSON schema. This was due to the usage of the Yup library (v0.32.11): transforming user inputs to schema-validated DTO with the cast() function creates objects with a prototype that contains default values from the yup schema.

Workaround

I'm not sure if MikroORM should fix this issue, as it happens in a very specific scenario. Ideally, JSON properties should not be polluted with prototypes. Feel free to close it or convert it into a discussion. I'm mostly posting for reference and to help others if anyone encounters the same problem.

What I did is that I overwrote Yup's cast() method to strip the resulting objects' prototypes, like so:

/**
 * Objects created by Yup are decorated with prototypes to store
 * metadata like default schema values, etc., which can have an impact on
 * downstream logic. For instance, it prevents MikroORM from comparing
 * diffs on managed entities.
 */
yup.addMethod(yup.object, "cast", function (value, options) {
  // @ts-expect-error use the original cast function, otherwise we get an infinite recursive loop
  const castedObject = this._cast(value, options);
  return stripPrototype(castedObject);
});

With the following code for the stripPrototype() method:

import { ObjectId } from "mongodb";

const ignoredClasses = [Date, ObjectId];

export function stripPrototype(obj: unknown) {
  // If the input is not an object or is null, return it as is (base case for recursion)
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }

  for (const ignoredClass of ignoredClasses) {
    if (obj instanceof ignoredClass) return obj;
  }

  if (Array.isArray(obj)) {
    return obj.map((item) => stripPrototype(item));
  }

  // Create a new object without prototype
  const cleanObj = Object.create(null);

  // Iterate over the object's own properties
  for (const key of Object.getOwnPropertyNames(obj)) {
    const value = obj[key];

    // Recursively strip prototype from sub-properties
    cleanObj[key] = stripPrototype(value);
  }

  return cleanObj;
}

Reproduction

Here is a simple test case to reproduce the issue: https://github.com/alexandre-abrioux/mikro-orm-issues/tree/iss-6050 ; this does not use the Yup library; I kept it simple.

What driver are you using?

@mikro-orm/mongodb

MikroORM version

6.3.10

Node.js version

18.20.4

Operating system

No response

Validations

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions