Skip to content

What makes a valid ObjectId #11227

@vkarpov15

Description

@vkarpov15

Discussed in #11221

Originally posted by Maximusya January 13, 2022
Right now I am migrating from Mongoose 5.x to 6.x and got about half of integration tests fail just because of undocumented changes in ObjectId validation.
These two mongoose validation methods differ in behavior both between the two in one version of mongoose and between mongoose versions (+dependencies):

  • mongoose.Types.ObjectId.isValid
  • mongoose.isValidObjectId

I get the expectation that once a variable has passed any of these validations, it can be safely specified both as new mongoose.Type.ObjectId(...) ctor arg, and any of MyModel.findById(..) et al.
Another kind of expectation (but probably far fetched): if a variable has passed any of these validations IT IS an id, but not a full document loaded from DB. Imagine a helper method that loads missing data if the argument passed in has properties that are not yet populated/loaded.

Example:

const mongoose = require('mongoose');

const testSubjects = {
  'null': null,
  'undefined': undefined,
  'new ObjectId()': new mongoose.Types.ObjectId(),
  'ObjectId().valueOf()': (new mongoose.Types.ObjectId()).valueOf(),
  'ObjectId().toString()': (new mongoose.Types.ObjectId()).toString(),
  'ObjectId().toHexString()': (new mongoose.Types.ObjectId()).toHexString(),
  '\'Z\'.repeat(12)': 'Z'.repeat(12),
  '\'F\'.repeat(24)': 'F'.repeat(24),
  '\'580e0797bb495f0a200e91ad\'': '580e0797bb495f0a200e91ad',
  '{ _id: new ObjectId() }': { _id: new mongoose.Types.ObjectId() },
  '{ _id: \'Z\'.repeat(12) }': { _id: 'Z'.repeat(12) },
  '{ _id: \'F\'.repeat(24) }': { _id: 'F'.repeat(24) },
  '{ _id: \'580e0797bb495f0a200e91ad\' }': { _id: '580e0797bb495f0a200e91ad' },
  '{ id: new ObjectId() }': { id: new mongoose.Types.ObjectId() },
  '{ id: \'Z\'.repeat(12) }': { id: 'Z'.repeat(12) },
  '{ id: \'F\'.repeat(24) }': { id: 'F'.repeat(24) },
  '{ id: \'580e0797bb495f0a200e91ad\' }': { id: '580e0797bb495f0a200e91ad' },
  '{ id: null }': { id: null },
  '{ id: undefined }': { id: undefined },
  '{ _id: null }': { _id: null },
  '{ _id: undefined }': { _id: undefined },
};

console.table(Object.entries(testSubjects).map(([key, value]) => ({
  test: key,
  'isValidObjectId': mongoose.isValidObjectId(value),
  'ObjectId.isValid': mongoose.Types.ObjectId.isValid(value),
  'new ObjectId': newObjectId(value),
})));

function newObjectId(value) {
  try {
    return new mongoose.Types.ObjectId(value);
  } catch (err) {
    return 'ERROR';
  }
}

[email protected] (same for @6.1.5) + [email protected]:

┌─────────┬───────────────────────────────────────┬─────────────────┬──────────────────┬──────────────────────────────────────────┐
│ (index) │                 test                  │ isValidObjectId │ ObjectId.isValid │               new ObjectId               │
├─────────┼───────────────────────────────────────┼─────────────────┼──────────────────┼──────────────────────────────────────────┤
│    0    │                'null'                 │      true       │      false       │ new ObjectId("61e117de7d6a06233f0cace6") │
│    1    │              'undefined'              │      true       │      false       │ new ObjectId("61e117de7d6a06233f0cace7") │
│    2    │           'new ObjectId()'            │      true       │       true       │ new ObjectId("61e117de7d6a06233f0cace0") │
│    3    │        'ObjectId().valueOf()'         │      true       │       true       │ new ObjectId("61e117de7d6a06233f0cace1") │
│    4    │        'ObjectId().toString()'        │      true       │       true       │ new ObjectId("61e117de7d6a06233f0cace2") │
│    5    │      'ObjectId().toHexString()'       │      true       │       true       │ new ObjectId("61e117de7d6a06233f0cace3") │
│    6    │           "'Z'.repeat(12)"            │      true       │       true       │ new ObjectId("5a5a5a5a5a5a5a5a5a5a5a5a") │
│    7    │           "'F'.repeat(24)"            │      true       │       true       │ new ObjectId("ffffffffffffffffffffffff") │
│    8    │     "'580e0797bb495f0a200e91ad'"      │      true       │       true       │ new ObjectId("580e0797bb495f0a200e91ad") │
│    9    │       '{ _id: new ObjectId() }'       │      true       │      false       │                 'ERROR'                  │
│   10    │       "{ _id: 'Z'.repeat(12) }"       │      true       │      false       │                 'ERROR'                  │
│   11    │       "{ _id: 'F'.repeat(24) }"       │      true       │      false       │                 'ERROR'                  │
│   12    │ "{ _id: '580e0797bb495f0a200e91ad' }" │      true       │      false       │                 'ERROR'                  │
│   13    │       '{ id: new ObjectId() }'        │      false      │      false       │                 'ERROR'                  │
│   14    │       "{ id: 'Z'.repeat(12) }"        │      false      │       true       │ new ObjectId("5a5a5a5a5a5a5a5a5a5a5a5a") │
│   15    │       "{ id: 'F'.repeat(24) }"        │      false      │       true       │ new ObjectId("ffffffffffffffffffffffff") │
│   16    │ "{ id: '580e0797bb495f0a200e91ad' }"  │      false      │       true       │ new ObjectId("580e0797bb495f0a200e91ad") │
│   17    │            '{ id: null }'             │      false      │      false       │                 'ERROR'                  │
│   18    │          '{ id: undefined }'          │      false      │      false       │                 'ERROR'                  │
│   19    │            '{ _id: null }'            │      false      │      false       │                 'ERROR'                  │
│   20    │         '{ _id: undefined }'          │      false      │      false       │                 'ERROR'                  │
└─────────┴───────────────────────────────────────┴─────────────────┴──────────────────┴──────────────────────────────────────────┘
$ npm ls bson
[email protected] /home/max/dev/mongoose616
└─┬ [email protected]
  ├── [email protected]
  └─┬ [email protected]
    └── [email protected] deduped

[email protected] + [email protected]

Note: [email protected] is demonstrated because since [email protected] { id: '580e0797bb495f0a200e91ad' } is considered a valid ObjectId. See #11209.
You can install this version today using version override in package.json:

  "overrides": {
    "bson": "4.6.0"
  }

┌─────────┬───────────────────────────────────────┬─────────────────┬──────────────────┬──────────────────────────────────────────┐
│ (index) │                 test                  │ isValidObjectId │ ObjectId.isValid │               new ObjectId               │
├─────────┼───────────────────────────────────────┼─────────────────┼──────────────────┼──────────────────────────────────────────┤
│    0    │                'null'                 │      true       │      false       │ new ObjectId("61e131d6bee71a108f689f2f") │
│    1    │              'undefined'              │      true       │      false       │ new ObjectId("61e131d6bee71a108f689f30") │
│    2    │           'new ObjectId()'            │      true       │       true       │ new ObjectId("61e131d6bee71a108f689f29") │
│    3    │        'ObjectId().valueOf()'         │      true       │       true       │ new ObjectId("61e131d6bee71a108f689f2a") │
│    4    │        'ObjectId().toString()'        │      true       │       true       │ new ObjectId("61e131d6bee71a108f689f2b") │
│    5    │      'ObjectId().toHexString()'       │      true       │       true       │ new ObjectId("61e131d6bee71a108f689f2c") │
│    6    │           "'Z'.repeat(12)"            │      true       │       true       │ new ObjectId("5a5a5a5a5a5a5a5a5a5a5a5a") │
│    7    │           "'F'.repeat(24)"            │      true       │       true       │ new ObjectId("ffffffffffffffffffffffff") │
│    8    │     "'580e0797bb495f0a200e91ad'"      │      true       │       true       │ new ObjectId("580e0797bb495f0a200e91ad") │
│    9    │       '{ _id: new ObjectId() }'       │      true       │      false       │                 'ERROR'                  │
│   10    │       "{ _id: 'Z'.repeat(12) }"       │      true       │      false       │                 'ERROR'                  │
│   11    │       "{ _id: 'F'.repeat(24) }"       │      true       │      false       │                 'ERROR'                  │
│   12    │ "{ _id: '580e0797bb495f0a200e91ad' }" │      true       │      false       │                 'ERROR'                  │
│   13    │       '{ id: new ObjectId() }'        │      false      │      false       │                 'ERROR'                  │
│   14    │       "{ id: 'Z'.repeat(12) }"        │      false      │      false       │ new ObjectId("5a5a5a5a5a5a5a5a5a5a5a5a") │
│   15    │       "{ id: 'F'.repeat(24) }"        │      false      │      false       │ new ObjectId("ffffffffffffffffffffffff") │
│   16    │ "{ id: '580e0797bb495f0a200e91ad' }"  │      false      │      false       │ new ObjectId("580e0797bb495f0a200e91ad") │
│   17    │            '{ id: null }'             │      false      │      false       │                 'ERROR'                  │
│   18    │          '{ id: undefined }'          │      false      │      false       │                 'ERROR'                  │
│   19    │            '{ _id: null }'            │      false      │      false       │                 'ERROR'                  │
│   20    │         '{ _id: undefined }'          │      false      │      false       │                 'ERROR'                  │
└─────────┴───────────────────────────────────────┴─────────────────┴──────────────────┴──────────────────────────────────────────┘
$ npm ls bson
[email protected] /home/max/dev/mongoose615bson460
└─┬ [email protected]
  ├── [email protected]
  └─┬ [email protected]
    └── [email protected] deduped

[email protected] + [email protected]

And some old versions (precisely from the codebase I am migrating now)

┌─────────┬───────────────────────────────────────┬─────────────────┬──────────────────┬──────────────────────────┐
│ (index) │                 test                  │ isValidObjectId │ ObjectId.isValid │       new ObjectId       │
├─────────┼───────────────────────────────────────┼─────────────────┼──────────────────┼──────────────────────────┤
│    0    │                'null'                 │      true       │      false       │ 61e13246a66c1202500f2c67 │
│    1    │              'undefined'              │      true       │      false       │ 61e13246a66c1202500f2c68 │
│    2    │           'new ObjectId()'            │      true       │       true       │ 61e13246a66c1202500f2c61 │
│    3    │        'ObjectId().valueOf()'         │      true       │       true       │ 61e13246a66c1202500f2c62 │
│    4    │        'ObjectId().toString()'        │      true       │       true       │ 61e13246a66c1202500f2c63 │
│    5    │      'ObjectId().toHexString()'       │      true       │       true       │ 61e13246a66c1202500f2c64 │
│    6    │           "'Z'.repeat(12)"            │      true       │       true       │ 5a5a5a5a5a5a5a5a5a5a5a5a │
│    7    │           "'F'.repeat(24)"            │      false      │       true       │ ffffffffffffffffffffffff │
│    8    │     "'580e0797bb495f0a200e91ad'"      │      true       │       true       │ 580e0797bb495f0a200e91ad │
│    9    │       '{ _id: new ObjectId() }'       │      true       │      false       │         'ERROR'          │
│   10    │       "{ _id: 'Z'.repeat(12) }"       │      true       │      false       │         'ERROR'          │
│   11    │       "{ _id: 'F'.repeat(24) }"       │      false      │      false       │         'ERROR'          │
│   12    │ "{ _id: '580e0797bb495f0a200e91ad' }" │      true       │      false       │         'ERROR'          │
│   13    │       '{ id: new ObjectId() }'        │      false      │      false       │         'ERROR'          │
│   14    │       "{ id: 'Z'.repeat(12) }"        │      false      │      false       │         'ERROR'          │
│   15    │       "{ id: 'F'.repeat(24) }"        │      false      │      false       │         'ERROR'          │
│   16    │ "{ id: '580e0797bb495f0a200e91ad' }"  │      false      │      false       │         'ERROR'          │
│   17    │            '{ id: null }'             │      false      │      false       │         'ERROR'          │
│   18    │          '{ id: undefined }'          │      false      │      false       │         'ERROR'          │
│   19    │            '{ _id: null }'            │      false      │      false       │         'ERROR'          │
│   20    │         '{ _id: undefined }'          │      false      │      false       │         'ERROR'          │
└─────────┴───────────────────────────────────────┴─────────────────┴──────────────────┴──────────────────────────┘
$ npm ls bson
[email protected] /home/max/dev/mongoose5123bson114
└─┬ [email protected]
  ├── [email protected] deduped
  └─┬ [email protected]
    └── [email protected] deduped

You can see how much has changed from those "old times".

Notable changes between versions

┌─────────┬───────────────────────────────────────┬─────────────────┬──────────────────┬──────────────────────────────────────────┐
│ (index) │                 test                  │ isValidObjectId │ ObjectId.isValid │               new ObjectId               │
├─────────┼───────────────────────────────────────┼─────────────────┼──────────────────┼──────────────────────────────────────────┤
[email protected] [email protected]:
│   16    │ "{ id: '580e0797bb495f0a200e91ad' }"  │      false      │       true       │ new ObjectId("580e0797bb495f0a200e91ad") │
[email protected] [email protected]:
│   16    │ "{ id: '580e0797bb495f0a200e91ad' }"  │      false      │      false       │ new ObjectId("580e0797bb495f0a200e91ad") │
[email protected] [email protected]:
│   16    │ "{ id: '580e0797bb495f0a200e91ad' }"  │      false      │      false       │         'ERROR'          │

┌─────────┬───────────────────────────────────────┬─────────────────┬──────────────────┬──────────────────────────────────────────┐
│ (index) │                 test                  │ isValidObjectId │ ObjectId.isValid │               new ObjectId               │
├─────────┼───────────────────────────────────────┼─────────────────┼──────────────────┼──────────────────────────────────────────┤
[email protected] [email protected], 4.6.1:
│    7    │           "'F'.repeat(24)"            │      true       │       true       │ new ObjectId("ffffffffffffffffffffffff") │
│   11    │       "{ _id: 'F'.repeat(24) }"       │      true       │      false       │                 'ERROR'                  │
[email protected] [email protected]:
│    7    │           "'F'.repeat(24)"            │      false      │       true       │ ffffffffffffffffffffffff │
│   11    │       "{ _id: 'F'.repeat(24) }"       │      false      │      false       │         'ERROR'          │

I can't help quoting a famous anecdote (translated):

A letter to the Balabanov Match Factory:
"I've been counting matches in your boxes for 11 years - there are 59, then 60, and sometimes 58. Are you all crazy there???"

MongoDB and mongoose seem to be mature products. And having a long history of changes/bugs with such base entity as ObjectId (regarding what is a valid ObjectId) - is appalling to say the least.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions