Skip to content

Commit 62e9447

Browse files
authored
Merge pull request Automattic#15184 from Automattic/vkarpov15/Automatticgh-11162
Schema.prototype.jsonSchema(): convert Mongoose Schema to JSON schema
2 parents 34bc2d1 + 57b48f9 commit 62e9447

22 files changed

+938
-1
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use strict';
2+
3+
/**
4+
* Handles creating `{ type: 'object' }` vs `{ bsonType: 'object' }` vs `{ bsonType: ['object', 'null'] }`
5+
*
6+
* @param {String} type
7+
* @param {String} bsonType
8+
* @param {Boolean} useBsonType
9+
* @param {Boolean} isRequired
10+
*/
11+
12+
module.exports = function createJSONSchemaTypeArray(type, bsonType, useBsonType, isRequired) {
13+
if (useBsonType) {
14+
if (isRequired) {
15+
return { bsonType };
16+
}
17+
return { bsonType: [bsonType, 'null'] };
18+
} else {
19+
if (isRequired) {
20+
return { type };
21+
}
22+
return { type: [type, 'null'] };
23+
}
24+
};

lib/schema.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2886,6 +2886,92 @@ Schema.prototype._preCompile = function _preCompile() {
28862886
this.plugin(idGetter, { deduplicate: true });
28872887
};
28882888

2889+
/**
2890+
* Returns a JSON schema representation of this Schema.
2891+
*
2892+
* By default, returns normal [JSON schema representation](https://json-schema.org/learn/getting-started-step-by-step), which is not typically what you want to use with
2893+
* [MongoDB's `$jsonSchema` collection option](https://www.mongodb.com/docs/manual/core/schema-validation/specify-json-schema/).
2894+
* Use the `useBsonType: true` option to return MongoDB `$jsonSchema` syntax instead.
2895+
*
2896+
* In addition to types, `jsonSchema()` supports the following Mongoose validators:
2897+
* - `enum` for strings and numbers
2898+
*
2899+
* #### Example:
2900+
* const schema = new Schema({ name: String });
2901+
* // { required: ['_id'], properties: { name: { type: ['string', 'null'] }, _id: { type: 'string' } } }
2902+
* schema.toJSONSchema();
2903+
*
2904+
* // { required: ['_id'], properties: { name: { bsonType: ['string', 'null'] }, _id: { bsonType: 'objectId' } } }
2905+
* schema.toJSONSchema({ useBsonType: true });
2906+
*
2907+
* @param {Object} [options]
2908+
* @param [Boolean] [options.useBsonType=false] if true, specify each path's type using `bsonType` rather than `type` for MongoDB $jsonSchema support
2909+
*/
2910+
2911+
Schema.prototype.toJSONSchema = function toJSONSchema(options) {
2912+
const useBsonType = options?.useBsonType ?? false;
2913+
const result = useBsonType ? { required: [], properties: {} } : { type: 'object', required: [], properties: {} };
2914+
for (const path of Object.keys(this.paths)) {
2915+
const schemaType = this.paths[path];
2916+
2917+
// Skip Map embedded paths, maps will be handled seperately.
2918+
if (schemaType._presplitPath.indexOf('$*') !== -1) {
2919+
continue;
2920+
}
2921+
2922+
// Nested paths are stored as `nested.path` in the schema type, so create nested paths in the json schema
2923+
// when necessary.
2924+
const isNested = schemaType._presplitPath.length > 1;
2925+
let jsonSchemaForPath = result;
2926+
if (isNested) {
2927+
for (let i = 0; i < schemaType._presplitPath.length - 1; ++i) {
2928+
const subpath = schemaType._presplitPath[i];
2929+
if (jsonSchemaForPath.properties[subpath] == null) {
2930+
jsonSchemaForPath.properties[subpath] = useBsonType
2931+
? {
2932+
bsonType: ['object', 'null'],
2933+
properties: {}
2934+
}
2935+
: {
2936+
type: ['object', 'null'],
2937+
properties: {}
2938+
};
2939+
}
2940+
jsonSchemaForPath = jsonSchemaForPath.properties[subpath];
2941+
}
2942+
}
2943+
2944+
const lastSubpath = schemaType._presplitPath[schemaType._presplitPath.length - 1];
2945+
let isRequired = false;
2946+
if (path === '_id') {
2947+
if (!jsonSchemaForPath.required) {
2948+
jsonSchemaForPath.required = [];
2949+
}
2950+
jsonSchemaForPath.required.push('_id');
2951+
isRequired = true;
2952+
} else if (schemaType.options.required && typeof schemaType.options.required !== 'function') {
2953+
if (!jsonSchemaForPath.required) {
2954+
jsonSchemaForPath.required = [];
2955+
}
2956+
// Only `required: true` paths are required, conditional required is not required
2957+
jsonSchemaForPath.required.push(lastSubpath);
2958+
isRequired = true;
2959+
}
2960+
jsonSchemaForPath.properties[lastSubpath] = schemaType.toJSONSchema(options);
2961+
if (schemaType.options.enum) {
2962+
jsonSchemaForPath.properties[lastSubpath].enum = isRequired
2963+
? schemaType.options.enum
2964+
: [...schemaType.options.enum, null];
2965+
}
2966+
}
2967+
2968+
// Otherwise MongoDB errors with "$jsonSchema keyword 'required' cannot be an empty array"
2969+
if (result.required.length === 0) {
2970+
delete result.required;
2971+
}
2972+
return result;
2973+
};
2974+
28892975
/*!
28902976
* Module exports.
28912977
*/

lib/schema/array.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const isOperator = require('../helpers/query/isOperator');
2121
const util = require('util');
2222
const utils = require('../utils');
2323
const castToNumber = require('./operators/helpers').castToNumber;
24+
const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeDefinition');
2425
const geospatial = require('./operators/geospatial');
2526
const getDiscriminatorByValue = require('../helpers/discriminator/getDiscriminatorByValue');
2627

@@ -700,6 +701,23 @@ handle.$ne = SchemaArray.prototype._castForQuery;
700701
handle.$nin = SchemaType.prototype.$conditionalHandlers.$nin;
701702
handle.$in = SchemaType.prototype.$conditionalHandlers.$in;
702703

704+
/**
705+
* Returns this schema type's representation in a JSON schema.
706+
*
707+
* @param [options]
708+
* @param [options.useBsonType=false] If true, return a representation with `bsonType` for use with MongoDB's `$jsonSchema`.
709+
* @returns {Object} JSON schema properties
710+
*/
711+
712+
SchemaArray.prototype.toJSONSchema = function toJSONSchema(options) {
713+
const embeddedSchemaType = this.getEmbeddedSchemaType();
714+
const isRequired = this.options.required && typeof this.options.required !== 'function';
715+
return {
716+
...createJSONSchemaTypeDefinition('array', 'array', options?.useBsonType, isRequired),
717+
items: embeddedSchemaType.toJSONSchema(options)
718+
};
719+
};
720+
703721
/*!
704722
* Module exports.
705723
*/

lib/schema/bigint.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
const CastError = require('../error/cast');
88
const SchemaType = require('../schemaType');
99
const castBigInt = require('../cast/bigint');
10+
const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeDefinition');
1011

1112
/**
1213
* BigInt SchemaType constructor.
@@ -240,6 +241,19 @@ SchemaBigInt.prototype._castNullish = function _castNullish(v) {
240241
return v;
241242
};
242243

244+
/**
245+
* Returns this schema type's representation in a JSON schema.
246+
*
247+
* @param [options]
248+
* @param [options.useBsonType=false] If true, return a representation with `bsonType` for use with MongoDB's `$jsonSchema`.
249+
* @returns {Object} JSON schema properties
250+
*/
251+
252+
SchemaBigInt.prototype.toJSONSchema = function toJSONSchema(options) {
253+
const isRequired = this.options.required && typeof this.options.required !== 'function';
254+
return createJSONSchemaTypeDefinition('string', 'long', options?.useBsonType, isRequired);
255+
};
256+
243257
/*!
244258
* Module exports.
245259
*/

lib/schema/boolean.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
const CastError = require('../error/cast');
88
const SchemaType = require('../schemaType');
99
const castBoolean = require('../cast/boolean');
10+
const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeDefinition');
1011

1112
/**
1213
* Boolean SchemaType constructor.
@@ -290,6 +291,19 @@ SchemaBoolean.prototype._castNullish = function _castNullish(v) {
290291
return v;
291292
};
292293

294+
/**
295+
* Returns this schema type's representation in a JSON schema.
296+
*
297+
* @param [options]
298+
* @param [options.useBsonType=false] If true, return a representation with `bsonType` for use with MongoDB's `$jsonSchema`.
299+
* @returns {Object} JSON schema properties
300+
*/
301+
302+
SchemaBoolean.prototype.toJSONSchema = function toJSONSchema(options) {
303+
const isRequired = this.options.required && typeof this.options.required !== 'function';
304+
return createJSONSchemaTypeDefinition('boolean', 'bool', options?.useBsonType, isRequired);
305+
};
306+
293307
/*!
294308
* Module exports.
295309
*/

lib/schema/buffer.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
const MongooseBuffer = require('../types/buffer');
88
const SchemaBufferOptions = require('../options/schemaBufferOptions');
99
const SchemaType = require('../schemaType');
10+
const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeDefinition');
1011
const handleBitwiseOperator = require('./operators/bitwise');
1112
const utils = require('../utils');
1213

@@ -300,6 +301,19 @@ SchemaBuffer.prototype.castForQuery = function($conditional, val, context) {
300301
return casted ? casted.toObject({ transform: false, virtuals: false }) : casted;
301302
};
302303

304+
/**
305+
* Returns this schema type's representation in a JSON schema.
306+
*
307+
* @param [options]
308+
* @param [options.useBsonType=false] If true, return a representation with `bsonType` for use with MongoDB's `$jsonSchema`.
309+
* @returns {Object} JSON schema properties
310+
*/
311+
312+
SchemaBuffer.prototype.toJSONSchema = function toJSONSchema(options) {
313+
const isRequired = this.options.required && typeof this.options.required !== 'function';
314+
return createJSONSchemaTypeDefinition('string', 'binData', options?.useBsonType, isRequired);
315+
};
316+
303317
/*!
304318
* Module exports.
305319
*/

lib/schema/date.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const MongooseError = require('../error/index');
88
const SchemaDateOptions = require('../options/schemaDateOptions');
99
const SchemaType = require('../schemaType');
1010
const castDate = require('../cast/date');
11+
const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeDefinition');
1112
const getConstructorName = require('../helpers/getConstructorName');
1213
const utils = require('../utils');
1314

@@ -426,6 +427,19 @@ SchemaDate.prototype.castForQuery = function($conditional, val, context) {
426427
return handler.call(this, val);
427428
};
428429

430+
/**
431+
* Returns this schema type's representation in a JSON schema.
432+
*
433+
* @param [options]
434+
* @param [options.useBsonType=false] If true, return a representation with `bsonType` for use with MongoDB's `$jsonSchema`.
435+
* @returns {Object} JSON schema properties
436+
*/
437+
438+
SchemaDate.prototype.toJSONSchema = function toJSONSchema(options) {
439+
const isRequired = this.options.required && typeof this.options.required !== 'function';
440+
return createJSONSchemaTypeDefinition('string', 'date', options?.useBsonType, isRequired);
441+
};
442+
429443
/*!
430444
* Module exports.
431445
*/

lib/schema/decimal128.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
const SchemaType = require('../schemaType');
88
const CastError = SchemaType.CastError;
99
const castDecimal128 = require('../cast/decimal128');
10+
const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeDefinition');
1011
const isBsonType = require('../helpers/isBsonType');
1112

1213
/**
@@ -221,6 +222,19 @@ SchemaDecimal128.prototype.$conditionalHandlers = {
221222
$lte: handleSingle
222223
};
223224

225+
/**
226+
* Returns this schema type's representation in a JSON schema.
227+
*
228+
* @param [options]
229+
* @param [options.useBsonType=false] If true, return a representation with `bsonType` for use with MongoDB's `$jsonSchema`.
230+
* @returns {Object} JSON schema properties
231+
*/
232+
233+
SchemaDecimal128.prototype.toJSONSchema = function toJSONSchema(options) {
234+
const isRequired = this.options.required && typeof this.options.required !== 'function';
235+
return createJSONSchemaTypeDefinition('string', 'decimal', options?.useBsonType, isRequired);
236+
};
237+
224238
/*!
225239
* Module exports.
226240
*/

lib/schema/documentArray.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const SchemaDocumentArrayOptions =
1212
require('../options/schemaDocumentArrayOptions');
1313
const SchemaType = require('../schemaType');
1414
const cast = require('../cast');
15+
const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeDefinition');
1516
const discriminator = require('../helpers/model/discriminator');
1617
const handleIdOption = require('../helpers/schema/handleIdOption');
1718
const handleSpreadDoc = require('../helpers/document/handleSpreadDoc');
@@ -651,6 +652,23 @@ function cast$elemMatch(val, context) {
651652
return cast(schema, val, null, this && this.$$context);
652653
}
653654

655+
/**
656+
* Returns this schema type's representation in a JSON schema.
657+
*
658+
* @param [options]
659+
* @param [options.useBsonType=false] If true, return a representation with `bsonType` for use with MongoDB's `$jsonSchema`.
660+
* @returns {Object} JSON schema properties
661+
*/
662+
663+
SchemaDocumentArray.prototype.toJSONSchema = function toJSONSchema(options) {
664+
const itemsTypeDefinition = createJSONSchemaTypeDefinition('object', 'object', options?.useBsonType, false);
665+
const isRequired = this.options.required && typeof this.options.required !== 'function';
666+
return {
667+
...createJSONSchemaTypeDefinition('array', 'array', options?.useBsonType, isRequired),
668+
items: { ...itemsTypeDefinition, ...this.schema.toJSONSchema(options) }
669+
};
670+
};
671+
654672
/*!
655673
* Module exports.
656674
*/

lib/schema/double.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
const CastError = require('../error/cast');
88
const SchemaType = require('../schemaType');
99
const castDouble = require('../cast/double');
10+
const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeDefinition');
1011

1112
/**
1213
* Double SchemaType constructor.
@@ -204,6 +205,18 @@ SchemaDouble.prototype.$conditionalHandlers = {
204205
$lte: handleSingle
205206
};
206207

208+
/**
209+
* Returns this schema type's representation in a JSON schema.
210+
*
211+
* @param [options]
212+
* @param [options.useBsonType=false] If true, return a representation with `bsonType` for use with MongoDB's `$jsonSchema`.
213+
* @returns {Object} JSON schema properties
214+
*/
215+
216+
SchemaDouble.prototype.toJSONSchema = function toJSONSchema(options) {
217+
const isRequired = this.options.required && typeof this.options.required !== 'function';
218+
return createJSONSchemaTypeDefinition('number', 'double', options?.useBsonType, isRequired);
219+
};
207220

208221
/*!
209222
* Module exports.

0 commit comments

Comments
 (0)