diff --git a/lib/document.js b/lib/document.js index e48c5a2b8b2..9b3c2fe81ac 100644 --- a/lib/document.js +++ b/lib/document.js @@ -55,7 +55,8 @@ const specialProperties = utils.specialProperties; * * @param {Object} obj the values to set * @param {Object} [fields] optional object containing the fields which were selected in the query returning this document and any populated paths data - * @param {Boolean} [skipId] bool, should we auto create an ObjectId _id + * @param {Object} [options] various configuration options for the document + * @param {Boolean} [options.defaults=true] if `false`, skip applying default values to this document. * @inherits NodeJS EventEmitter http://nodejs.org/api/events.html#events_class_events_eventemitter * @event `init`: Emitted on a document after it has been retrieved from the db and fully hydrated by Mongoose. * @event `save`: Emitted when the document is successfully saved @@ -67,7 +68,9 @@ function Document(obj, fields, skipId, options) { options = skipId; skipId = options.skipId; } - options = options || {}; + options = Object.assign({}, options); + const defaults = get(options, 'defaults', true); + options.defaults = defaults; // Support `browserDocument.js` syntax if (this.schema == null) { @@ -126,9 +129,11 @@ function Document(obj, fields, skipId, options) { // By default, defaults get applied **before** setting initial values // Re: gh-6155 - $__applyDefaults(this, fields, skipId, exclude, hasIncludedChildren, true, { - isNew: this.isNew - }); + if (defaults) { + $__applyDefaults(this, fields, skipId, exclude, hasIncludedChildren, true, { + isNew: this.isNew + }); + } } if (obj) { @@ -147,13 +152,13 @@ function Document(obj, fields, skipId, options) { // Function defaults get applied **after** setting initial values so they // see the full doc rather than an empty one, unless they opt out. // Re: gh-3781, gh-6155 - if (options.willInit) { + if (options.willInit && defaults) { EventEmitter.prototype.once.call(this, 'init', () => { $__applyDefaults(this, fields, skipId, exclude, hasIncludedChildren, false, options.skipDefaults, { isNew: this.isNew }); }); - } else { + } else if (defaults) { $__applyDefaults(this, fields, skipId, exclude, hasIncludedChildren, false, options.skipDefaults, { isNew: this.isNew }); diff --git a/lib/types/subdocument.js b/lib/types/subdocument.js index 521237aee72..63cd42c0c11 100644 --- a/lib/types/subdocument.js +++ b/lib/types/subdocument.js @@ -29,7 +29,10 @@ function Subdocument(value, fields, parent, skipId, options) { } if (parent != null) { // If setting a nested path, should copy isNew from parent re: gh-7048 - options = Object.assign({}, options, { isNew: parent.isNew }); + options = Object.assign({}, options, { + isNew: parent.isNew, + defaults: parent.$__.$options.defaults + }); } Document.call(this, value, fields, skipId, options); diff --git a/test/document.test.js b/test/document.test.js index 9983736d38a..8d3f6bcae64 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -8975,4 +8975,25 @@ describe('document', function() { const axl = new Person({ fullName: 'Axl Rose' }); assert.equal(axl.fullName, 'Axl Rose'); }); + + it('supports skipping defaults on a document (gh-8271)', function() { + const testSchema = new mongoose.Schema({ + testTopLevel: { type: String, default: 'foo' }, + testNested: { + prop: { type: String, default: 'bar' } + }, + testArray: [{ prop: { type: String, defualt: 'baz' } }], + testSingleNested: new Schema({ + prop: { type: String, default: 'qux' } + }) + }); + const Test = db.model('Test', testSchema); + + const doc = new Test({ testArray: [{}], testSingleNested: {} }, null, + { defaults: false }); + assert.ok(!doc.testTopLevel); + assert.ok(!doc.testNested.prop); + assert.ok(!doc.testArray[0].prop); + assert.ok(!doc.testSingleNested.prop); + }); });