Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Schema.prototype.jsonSchema(): convert Mongoose Schema to JSON schema #15184

Merged
merged 18 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading