Skip to content

Commit

Permalink
Merge pull request #15184 from Automattic/vkarpov15/gh-11162
Browse files Browse the repository at this point in the history
Schema.prototype.jsonSchema(): convert Mongoose Schema to JSON schema
  • Loading branch information
vkarpov15 authored Jan 20, 2025
2 parents 34bc2d1 + 57b48f9 commit 62e9447
Show file tree
Hide file tree
Showing 22 changed files with 938 additions and 1 deletion.
24 changes: 24 additions & 0 deletions lib/helpers/createJSONSchemaTypeDefinition.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict';

/**
* Handles creating `{ type: 'object' }` vs `{ bsonType: 'object' }` vs `{ bsonType: ['object', 'null'] }`
*
* @param {String} type
* @param {String} bsonType
* @param {Boolean} useBsonType
* @param {Boolean} isRequired
*/

module.exports = function createJSONSchemaTypeArray(type, bsonType, useBsonType, isRequired) {
if (useBsonType) {
if (isRequired) {
return { bsonType };
}
return { bsonType: [bsonType, 'null'] };
} else {
if (isRequired) {
return { type };
}
return { type: [type, 'null'] };
}
};
86 changes: 86 additions & 0 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -2886,6 +2886,92 @@ Schema.prototype._preCompile = function _preCompile() {
this.plugin(idGetter, { deduplicate: true });
};

/**
* Returns a JSON schema representation of this Schema.
*
* 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
* [MongoDB's `$jsonSchema` collection option](https://www.mongodb.com/docs/manual/core/schema-validation/specify-json-schema/).
* Use the `useBsonType: true` option to return MongoDB `$jsonSchema` syntax instead.
*
* In addition to types, `jsonSchema()` supports the following Mongoose validators:
* - `enum` for strings and numbers
*
* #### Example:
* const schema = new Schema({ name: String });
* // { required: ['_id'], properties: { name: { type: ['string', 'null'] }, _id: { type: 'string' } } }
* schema.toJSONSchema();
*
* // { required: ['_id'], properties: { name: { bsonType: ['string', 'null'] }, _id: { bsonType: 'objectId' } } }
* schema.toJSONSchema({ useBsonType: true });
*
* @param {Object} [options]
* @param [Boolean] [options.useBsonType=false] if true, specify each path's type using `bsonType` rather than `type` for MongoDB $jsonSchema support
*/

Schema.prototype.toJSONSchema = function toJSONSchema(options) {
const useBsonType = options?.useBsonType ?? false;
const result = useBsonType ? { required: [], properties: {} } : { type: 'object', required: [], properties: {} };
for (const path of Object.keys(this.paths)) {
const schemaType = this.paths[path];

// Skip Map embedded paths, maps will be handled seperately.
if (schemaType._presplitPath.indexOf('$*') !== -1) {
continue;
}

// Nested paths are stored as `nested.path` in the schema type, so create nested paths in the json schema
// when necessary.
const isNested = schemaType._presplitPath.length > 1;
let jsonSchemaForPath = result;
if (isNested) {
for (let i = 0; i < schemaType._presplitPath.length - 1; ++i) {
const subpath = schemaType._presplitPath[i];
if (jsonSchemaForPath.properties[subpath] == null) {
jsonSchemaForPath.properties[subpath] = useBsonType
? {
bsonType: ['object', 'null'],
properties: {}
}
: {
type: ['object', 'null'],
properties: {}
};
}
jsonSchemaForPath = jsonSchemaForPath.properties[subpath];
}
}

const lastSubpath = schemaType._presplitPath[schemaType._presplitPath.length - 1];
let isRequired = false;
if (path === '_id') {
if (!jsonSchemaForPath.required) {
jsonSchemaForPath.required = [];
}
jsonSchemaForPath.required.push('_id');
isRequired = true;
} else if (schemaType.options.required && typeof schemaType.options.required !== 'function') {
if (!jsonSchemaForPath.required) {
jsonSchemaForPath.required = [];
}
// Only `required: true` paths are required, conditional required is not required
jsonSchemaForPath.required.push(lastSubpath);
isRequired = true;
}
jsonSchemaForPath.properties[lastSubpath] = schemaType.toJSONSchema(options);
if (schemaType.options.enum) {
jsonSchemaForPath.properties[lastSubpath].enum = isRequired
? schemaType.options.enum
: [...schemaType.options.enum, null];
}
}

// Otherwise MongoDB errors with "$jsonSchema keyword 'required' cannot be an empty array"
if (result.required.length === 0) {
delete result.required;
}
return result;
};

/*!
* Module exports.
*/
Expand Down
18 changes: 18 additions & 0 deletions lib/schema/array.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const isOperator = require('../helpers/query/isOperator');
const util = require('util');
const utils = require('../utils');
const castToNumber = require('./operators/helpers').castToNumber;
const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeDefinition');
const geospatial = require('./operators/geospatial');
const getDiscriminatorByValue = require('../helpers/discriminator/getDiscriminatorByValue');

Expand Down Expand Up @@ -700,6 +701,23 @@ handle.$ne = SchemaArray.prototype._castForQuery;
handle.$nin = SchemaType.prototype.$conditionalHandlers.$nin;
handle.$in = SchemaType.prototype.$conditionalHandlers.$in;

/**
* Returns this schema type's representation in a JSON schema.
*
* @param [options]
* @param [options.useBsonType=false] If true, return a representation with `bsonType` for use with MongoDB's `$jsonSchema`.
* @returns {Object} JSON schema properties
*/

SchemaArray.prototype.toJSONSchema = function toJSONSchema(options) {
const embeddedSchemaType = this.getEmbeddedSchemaType();
const isRequired = this.options.required && typeof this.options.required !== 'function';
return {
...createJSONSchemaTypeDefinition('array', 'array', options?.useBsonType, isRequired),
items: embeddedSchemaType.toJSONSchema(options)
};
};

/*!
* Module exports.
*/
Expand Down
14 changes: 14 additions & 0 deletions lib/schema/bigint.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
const CastError = require('../error/cast');
const SchemaType = require('../schemaType');
const castBigInt = require('../cast/bigint');
const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeDefinition');

/**
* BigInt SchemaType constructor.
Expand Down Expand Up @@ -240,6 +241,19 @@ SchemaBigInt.prototype._castNullish = function _castNullish(v) {
return v;
};

/**
* Returns this schema type's representation in a JSON schema.
*
* @param [options]
* @param [options.useBsonType=false] If true, return a representation with `bsonType` for use with MongoDB's `$jsonSchema`.
* @returns {Object} JSON schema properties
*/

SchemaBigInt.prototype.toJSONSchema = function toJSONSchema(options) {
const isRequired = this.options.required && typeof this.options.required !== 'function';
return createJSONSchemaTypeDefinition('string', 'long', options?.useBsonType, isRequired);
};

/*!
* Module exports.
*/
Expand Down
14 changes: 14 additions & 0 deletions lib/schema/boolean.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
const CastError = require('../error/cast');
const SchemaType = require('../schemaType');
const castBoolean = require('../cast/boolean');
const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeDefinition');

/**
* Boolean SchemaType constructor.
Expand Down Expand Up @@ -290,6 +291,19 @@ SchemaBoolean.prototype._castNullish = function _castNullish(v) {
return v;
};

/**
* Returns this schema type's representation in a JSON schema.
*
* @param [options]
* @param [options.useBsonType=false] If true, return a representation with `bsonType` for use with MongoDB's `$jsonSchema`.
* @returns {Object} JSON schema properties
*/

SchemaBoolean.prototype.toJSONSchema = function toJSONSchema(options) {
const isRequired = this.options.required && typeof this.options.required !== 'function';
return createJSONSchemaTypeDefinition('boolean', 'bool', options?.useBsonType, isRequired);
};

/*!
* Module exports.
*/
Expand Down
14 changes: 14 additions & 0 deletions lib/schema/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
const MongooseBuffer = require('../types/buffer');
const SchemaBufferOptions = require('../options/schemaBufferOptions');
const SchemaType = require('../schemaType');
const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeDefinition');
const handleBitwiseOperator = require('./operators/bitwise');
const utils = require('../utils');

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

/**
* Returns this schema type's representation in a JSON schema.
*
* @param [options]
* @param [options.useBsonType=false] If true, return a representation with `bsonType` for use with MongoDB's `$jsonSchema`.
* @returns {Object} JSON schema properties
*/

SchemaBuffer.prototype.toJSONSchema = function toJSONSchema(options) {
const isRequired = this.options.required && typeof this.options.required !== 'function';
return createJSONSchemaTypeDefinition('string', 'binData', options?.useBsonType, isRequired);
};

/*!
* Module exports.
*/
Expand Down
14 changes: 14 additions & 0 deletions lib/schema/date.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const MongooseError = require('../error/index');
const SchemaDateOptions = require('../options/schemaDateOptions');
const SchemaType = require('../schemaType');
const castDate = require('../cast/date');
const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeDefinition');
const getConstructorName = require('../helpers/getConstructorName');
const utils = require('../utils');

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

/**
* Returns this schema type's representation in a JSON schema.
*
* @param [options]
* @param [options.useBsonType=false] If true, return a representation with `bsonType` for use with MongoDB's `$jsonSchema`.
* @returns {Object} JSON schema properties
*/

SchemaDate.prototype.toJSONSchema = function toJSONSchema(options) {
const isRequired = this.options.required && typeof this.options.required !== 'function';
return createJSONSchemaTypeDefinition('string', 'date', options?.useBsonType, isRequired);
};

/*!
* Module exports.
*/
Expand Down
14 changes: 14 additions & 0 deletions lib/schema/decimal128.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
const SchemaType = require('../schemaType');
const CastError = SchemaType.CastError;
const castDecimal128 = require('../cast/decimal128');
const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeDefinition');
const isBsonType = require('../helpers/isBsonType');

/**
Expand Down Expand Up @@ -221,6 +222,19 @@ SchemaDecimal128.prototype.$conditionalHandlers = {
$lte: handleSingle
};

/**
* Returns this schema type's representation in a JSON schema.
*
* @param [options]
* @param [options.useBsonType=false] If true, return a representation with `bsonType` for use with MongoDB's `$jsonSchema`.
* @returns {Object} JSON schema properties
*/

SchemaDecimal128.prototype.toJSONSchema = function toJSONSchema(options) {
const isRequired = this.options.required && typeof this.options.required !== 'function';
return createJSONSchemaTypeDefinition('string', 'decimal', options?.useBsonType, isRequired);
};

/*!
* Module exports.
*/
Expand Down
18 changes: 18 additions & 0 deletions lib/schema/documentArray.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const SchemaDocumentArrayOptions =
require('../options/schemaDocumentArrayOptions');
const SchemaType = require('../schemaType');
const cast = require('../cast');
const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeDefinition');
const discriminator = require('../helpers/model/discriminator');
const handleIdOption = require('../helpers/schema/handleIdOption');
const handleSpreadDoc = require('../helpers/document/handleSpreadDoc');
Expand Down Expand Up @@ -651,6 +652,23 @@ function cast$elemMatch(val, context) {
return cast(schema, val, null, this && this.$$context);
}

/**
* Returns this schema type's representation in a JSON schema.
*
* @param [options]
* @param [options.useBsonType=false] If true, return a representation with `bsonType` for use with MongoDB's `$jsonSchema`.
* @returns {Object} JSON schema properties
*/

SchemaDocumentArray.prototype.toJSONSchema = function toJSONSchema(options) {
const itemsTypeDefinition = createJSONSchemaTypeDefinition('object', 'object', options?.useBsonType, false);
const isRequired = this.options.required && typeof this.options.required !== 'function';
return {
...createJSONSchemaTypeDefinition('array', 'array', options?.useBsonType, isRequired),
items: { ...itemsTypeDefinition, ...this.schema.toJSONSchema(options) }
};
};

/*!
* Module exports.
*/
Expand Down
13 changes: 13 additions & 0 deletions lib/schema/double.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
const CastError = require('../error/cast');
const SchemaType = require('../schemaType');
const castDouble = require('../cast/double');
const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeDefinition');

/**
* Double SchemaType constructor.
Expand Down Expand Up @@ -204,6 +205,18 @@ SchemaDouble.prototype.$conditionalHandlers = {
$lte: handleSingle
};

/**
* Returns this schema type's representation in a JSON schema.
*
* @param [options]
* @param [options.useBsonType=false] If true, return a representation with `bsonType` for use with MongoDB's `$jsonSchema`.
* @returns {Object} JSON schema properties
*/

SchemaDouble.prototype.toJSONSchema = function toJSONSchema(options) {
const isRequired = this.options.required && typeof this.options.required !== 'function';
return createJSONSchemaTypeDefinition('number', 'double', options?.useBsonType, isRequired);
};

/*!
* Module exports.
Expand Down
14 changes: 14 additions & 0 deletions lib/schema/int32.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
const CastError = require('../error/cast');
const SchemaType = require('../schemaType');
const castInt32 = require('../cast/int32');
const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeDefinition');
const handleBitwiseOperator = require('./operators/bitwise');

/**
Expand Down Expand Up @@ -246,6 +247,19 @@ SchemaInt32.prototype.castForQuery = function($conditional, val, context) {
}
};

/**
* Returns this schema type's representation in a JSON schema.
*
* @param [options]
* @param [options.useBsonType=false] If true, return a representation with `bsonType` for use with MongoDB's `$jsonSchema`.
* @returns {Object} JSON schema properties
*/

SchemaInt32.prototype.toJSONSchema = function toJSONSchema(options) {
const isRequired = this.options.required && typeof this.options.required !== 'function';
return createJSONSchemaTypeDefinition('number', 'int', options?.useBsonType, isRequired);
};


/*!
* Module exports.
Expand Down
Loading

0 comments on commit 62e9447

Please sign in to comment.