#16373·mongoose

toObject({getters:true}) on projected documents: applyGetters iterates all schema paths and each unselected path triggers an O(projectionKeys) isSelected scan — O(paths x projection) per doc

Author: tsushanthCreated Jul 7, 2026Updated Jul 28, 2026
Labelsperformance

toObject({getters:true}) on projected documents: applyGetters iterates all schema paths and each unselected path triggers an O(projectionKeys) isSelected scan — O(paths x projection) per doc

Repo: Automattic/mongoose Location: lib/document.js:4394 (applyGetters per-path $__isSelected call) x lib/document.js:2572 (isSelected projection-key scan) Severity: medium · Confidence: 0.75 Type: complexity-at-a-distance

Description

applyGetters in lib/document.js (line ~4371) loops over every key of schema.paths and calls self.$__isSelected(path) for each. Document.prototype.isSelected (line ~2525) is O(S) for any path not literally present in the projection: after the O(1) path in this.$__.selected check fails, it loops all projection keys doing startsWith comparisons (lines ~2572-2584). So serializing one projected document with getters enabled (a very common toJSON config) costs O(P x S) where P = schema paths and S = projection keys — per document, per serialization. The chain crosses three modules: lib/query.js _completeMany builds projected docs (fields propagated into the Document constructor, $__.selected set at lib/document.js:158), then user/toJSON triggers $toObject -> applyGetters -> isSelected. Each function looks fine locally; the product only appears across the boundary.

Benchmark

With schema width 2N and an inclusive projection of N keys, toObject({getters:true}) time should grow superlinearly, tracking the O(N^2) prediction (N=500 hundreds-to-thousands of times slower per pass than N=10) because each of the ~N unselected paths scans the N projection keys.

Observed complexity: O(N·M)

| Input | median_ms | ratio | note |
|-------|-----------|-------|------|
| N=10 | 2.75 | 1.00 | — |
| N=50 | 17.59 | 6.40 | — |
| N=100 | 68.20 | 24.79 | — |
| N=500 | 2199.44 | 799.43 | — |

benchmark confirmed