// listCollections tests expect that a collection is not implicitly created after a drop. // @tags: [ // assumes_no_implicit_collection_creation_after_drop, // requires_non_retryable_commands, // ] /** * Tests for JSON Schema document validation. */ (function() { "use strict"; load("jstests/libs/assert_schema_match.js"); load("jstests/libs/sbe_util.js"); // For checkSBEEnabled. const isSBEEnabled = checkSBEEnabled(db); let coll = db.jstests_json_schema; coll.drop(); assert.commandWorked(coll.insert({_id: 0, num: 3})); assert.commandWorked(coll.insert({_id: 1, num: -3})); assert.commandWorked(coll.insert({_id: 2, num: NumberInt(2)})); assert.commandWorked(coll.insert({_id: 3, num: NumberInt(-2)})); assert.commandWorked(coll.insert({_id: 4, num: NumberLong(1)})); assert.commandWorked(coll.insert({_id: 5, num: NumberLong(-1)})); assert.commandWorked(coll.insert({_id: 6, num: {}})); assert.commandWorked(coll.insert({_id: 7, num: "str"})); assert.commandWorked(coll.insert({_id: 8, num: "string"})); assert.commandWorked(coll.insert({_id: 9})); // Test that $jsonSchema fails to parse if its argument is not an object. assert.throws(function() { coll.find({$jsonSchema: "foo"}).itcount(); }); assert.throws(function() { coll.find({$jsonSchema: []}).itcount(); }); // Test that $jsonSchema fails to parse if the value for the "type" keyword is not a string. assert.throws(function() { coll.find({$jsonSchema: {type: 3}}).itcount(); }); assert.throws(function() { coll.find({$jsonSchema: {type: {}}}).itcount(); }); // Test that $jsonSchema fails to parse if the value for the "type" keyword is an unsupported // alias. assert.throws(function() { coll.find({$jsonSchema: {type: 'integer'}}).itcount(); }); // Test that $jsonSchema fails to parse if the value for the properties keyword is not an // object. assert.throws(function() { coll.find({$jsonSchema: {properties: 3}}).itcount(); }); assert.throws(function() { coll.find({$jsonSchema: {properties: []}}).itcount(); }); // Test that $jsonSchema fails to parse if one of the properties named inside the argument for // the properties keyword is not an object. assert.throws(function() { coll.find({$jsonSchema: {properties: {num: "number"}}}).itcount(); }); // Test that $jsonSchema fails to parse if the values for the maximum, maxLength, and // minlength keywords are not numbers. assert.throws(function() { coll.find({$jsonSchema: {properties: {num: {maximum: "0"}}}}).itcount(); }); assert.throws(function() { coll.find({$jsonSchema: {properties: {num: {maximum: {}}}}}).itcount(); }); assert.throws(function() { coll.find({$jsonSchema: {properties: {num: {maxLength: "0"}}}}).itcount(); }); assert.throws(function() { coll.find({$jsonSchema: {properties: {num: {maxLength: {}}}}}).itcount(); }); assert.throws(function() { coll.find({$jsonSchema: {properties: {num: {minLength: "0"}}}}).itcount(); }); assert.throws(function() { coll.find({$jsonSchema: {properties: {num: {minLength: {}}}}}).itcount(); }); // Test that the empty schema matches everything. assert.eq(10, coll.find({$jsonSchema: {}}).itcount()); // Test that a schema just checking that the type of stored documents is "object" is legal and // matches everything. assert.eq(10, coll.find({$jsonSchema: {type: "object"}}).itcount()); // Test that schemas whose top-level type is not object matches nothing. assert.eq(0, coll.find({$jsonSchema: {type: "string"}}).itcount()); assert.eq(0, coll.find({$jsonSchema: {bsonType: "long"}}).itcount()); assert.eq(0, coll.find({$jsonSchema: {bsonType: "objectId"}}).itcount()); // Test that type:"number" only matches numbers, or documents where the field is missing. assert.eq([{_id: 0}, {_id: 1}, {_id: 2}, {_id: 3}, {_id: 4}, {_id: 5}, {_id: 9}], coll.find({$jsonSchema: {properties: {num: {type: "number"}}}}, {_id: 1}) .sort({_id: 1}) .toArray()); // Test that maximum restriction is enforced correctly. assert.eq([{_id: 1}, {_id: 3}, {_id: 5}, {_id: 9}], coll.find({$jsonSchema: {properties: {num: {type: "number", maximum: -1}}}}, {_id: 1}) .sort({_id: 1}) .toArray()); // Repeat the test, but include an explicit top-level type:"object". assert.eq( [{_id: 1}, {_id: 3}, {_id: 5}, {_id: 9}], coll.find({$jsonSchema: {type: "object", properties: {num: {type: "number", maximum: -1}}}}, {_id: 1}) .sort({_id: 1}) .toArray()); // Test that type:"long" only matches longs, or documents where the field is missing. assert.eq([{_id: 4}, {_id: 5}, {_id: 9}], coll.find({$jsonSchema: {properties: {num: {bsonType: "long"}}}}, {_id: 1}) .sort({_id: 1}) .toArray()); // Test that maximum restriction is enforced correctly with type:"long". assert.eq([{_id: 5}, {_id: 9}], coll.find({$jsonSchema: {properties: {num: {bsonType: "long", maximum: 0}}}}, {_id: 1}) .sort({_id: 1}) .toArray()); // Test that maximum restriction without a numeric type specified only applies to numbers. assert.eq( [{_id: 1}, {_id: 3}, {_id: 5}, {_id: 6}, {_id: 7}, {_id: 8}, {_id: 9}], coll.find({$jsonSchema: {properties: {num: {maximum: 0}}}}, {_id: 1}).sort({_id: 1}).toArray()); // Test that maximum restriction does nothing if a non-numeric type is also specified. assert.eq([{_id: 7}, {_id: 8}, {_id: 9}], coll.find({$jsonSchema: {properties: {num: {type: "string", maximum: 0}}}}, {_id: 1}) .sort({_id: 1}) .toArray()); // Test that maxLength restriction doesn't return strings with length greater than maxLength. assert.eq([{_id: 9}], coll.find({$jsonSchema: {properties: {num: {type: "string", maxLength: 2}}}}, {_id: 1}) .sort({_id: 1}) .toArray()); // Test that maxLength restriction returns strings with length less than or equal to maxLength. assert.eq([{_id: 7}, {_id: 9}], coll.find({$jsonSchema: {properties: {num: {type: "string", maxLength: 3}}}}, {_id: 1}) .sort({_id: 1}) .toArray()); // Test that minLength restriction doesn't return strings with length less than minLength. assert.eq([{_id: 8}, {_id: 9}], coll.find({$jsonSchema: {properties: {num: {type: "string", minLength: 4}}}}, {_id: 1}) .sort({_id: 1}) .toArray()); // Test that minLength restriction returns strings with length greater than or equal to // minLength. assert.eq([{_id: 7}, {_id: 8}, {_id: 9}], coll.find({$jsonSchema: {properties: {num: {type: "string", minLength: 3}}}}, {_id: 1}) .sort({_id: 1}) .toArray()); // Test that $jsonSchema fails to parse if the values for the pattern keyword is not a string. assert.throws(function() { coll.find({$jsonSchema: {properties: {num: {pattern: 0}}}}).itcount(); }); assert.throws(function() { coll.find({$jsonSchema: {properties: {num: {pattern: {}}}}}).itcount(); }); // Tests that the pattern keyword only returns strings that match the regex pattern. assert.eq([{_id: 8}, {_id: 9}], coll.find({$jsonSchema: {properties: {num: {type: "string", pattern: "ing"}}}}, {_id: 1}) .sort({_id: 1}) .toArray()); coll.drop(); assert.commandWorked(coll.insert({_id: 0, obj: 3})); assert.commandWorked(coll.insert({_id: 1, obj: {f1: {f3: "str"}, f2: "str"}})); assert.commandWorked(coll.insert({_id: 2, obj: {f1: "str", f2: "str"}})); assert.commandWorked(coll.insert({_id: 3, obj: {f1: 1, f2: "str"}})); // Test that properties keyword can be used recursively, and that it does not apply when the // field does not contain on object. assert.eq([{_id: 0}, {_id: 1}], coll.find({ $jsonSchema: { properties: { obj: { properties: { f1: {type: "object", properties: {f3: {type: "string"}}}, f2: {type: "string"} } } } } }, {_id: 1}) .sort({_id: 1}) .toArray()); // Test that $jsonSchema can be combined with other operators in the match language. assert.eq([{_id: 0}, {_id: 1}, {_id: 2}], coll.find({ $or: [ {"obj.f1": "str"}, { $jsonSchema: { properties: { obj: { properties: { f1: {type: "object", properties: {f3: {type: "string"}}}, f2: {type: "string"} } } } } } ] }, {_id: 1}) .sort({_id: 1}) .toArray()); coll.drop(); assert.commandWorked(coll.insert({_id: 0, arr: 3})); assert.commandWorked(coll.insert({_id: 1, arr: [1, "foo"]})); assert.commandWorked(coll.insert({_id: 2, arr: [{a: 1}, {b: 2}]})); assert.commandWorked(coll.insert({_id: 3, arr: []})); assert.commandWorked(coll.insert({_id: 4, arr: {a: []}})); // Test that the type:"array" restriction works as expected. assert.eq([{_id: 1}, {_id: 2}, {_id: 3}], coll.find({$jsonSchema: {properties: {arr: {type: "array"}}}}, {_id: 1}) .sort({_id: 1}) .toArray()); // Test that type:"number" works correctly in the presence of arrays. assert.eq([{_id: 0}], coll.find({$jsonSchema: {properties: {arr: {type: "number"}}}}, {_id: 1}) .sort({_id: 1}) .toArray()); // Test that the following keywords fail to parse although present in the spec: // - default // - definitions // - format // - id // - $ref // - $schema let res = coll.runCommand({find: coll.getName(), query: {$jsonSchema: {default: {_id: 0}}}}); assert.commandFailedWithCode(res, [ErrorCodes.FailedToParse, 40415]); res = coll.runCommand( {find: coll.getName(), query: {$jsonSchema: {definitions: {numberField: {type: "number"}}}}}); assert.commandFailedWithCode(res, [ErrorCodes.FailedToParse, 40415]); res = coll.runCommand({find: coll.getName(), query: {$jsonSchema: {format: "email"}}}); assert.commandFailedWithCode(res, [ErrorCodes.FailedToParse, 40415]); res = coll.runCommand({find: coll.getName(), query: {$jsonSchema: {id: "someschema.json"}}}); assert.commandFailedWithCode(res, [ErrorCodes.FailedToParse, 40415]); res = coll.runCommand({ find: coll.getName(), query: {$jsonSchema: {properties: {a: {$ref: "#/definitions/positiveInt"}}}} }); assert.commandFailedWithCode(res, [ErrorCodes.FailedToParse, 40415]); res = coll.runCommand({find: coll.getName(), query: {$jsonSchema: {$schema: "hyper-schema"}}}); assert.commandFailedWithCode(res, [ErrorCodes.FailedToParse, 40415]); res = coll.runCommand({ find: coll.getName(), query: {$jsonSchema: {$schema: "http://json-schema.org/draft-04/schema#"}} }); assert.commandFailedWithCode(res, [ErrorCodes.FailedToParse, 40415]); // Test that the following allowlisted keywords are verified as strings but otherwise ignored // in a top-level schema: // - description // - title assertSchemaMatch(coll, {description: "test"}, {}, true); assertSchemaMatch(coll, {title: "insert title"}, {}, true); // Repeat the test above with nested schema. assertSchemaMatch(coll, {properties: {a: {description: "test"}}}, {a: {}}, true); assertSchemaMatch(coll, {properties: {a: {title: "this is a's title"}}}, {a: {}}, true); // Test that the $jsonSchema validator is correctly stored in the collection catalog. coll.drop(); let schema = {properties: {a: {type: 'number'}, b: {minLength: 1}}}; assert.commandWorked(db.createCollection(coll.getName(), {validator: {$jsonSchema: schema}})); let listCollectionsOutput = db.runCommand({listCollections: 1, filter: {name: coll.getName()}}); assert.commandWorked(listCollectionsOutput); assert.eq(listCollectionsOutput.cursor.firstBatch[0].options.validator, {$jsonSchema: schema}); // Repeat the test above using the allowlisted metadata keywords. coll.drop(); schema = { title: "Test schema", description: "Metadata keyword test" }; assert.commandWorked(db.createCollection(coll.getName(), {validator: {$jsonSchema: schema}})); listCollectionsOutput = db.runCommand({listCollections: 1, filter: {name: coll.getName()}}); assert.commandWorked(listCollectionsOutput); assert.eq(listCollectionsOutput.cursor.firstBatch[0].options.validator, {$jsonSchema: schema}); // Repeat again with a nested schema. coll.drop(); schema = { properties: {a: {title: "Nested title", description: "Nested description"}} }; assert.commandWorked(db.createCollection(coll.getName(), {validator: {$jsonSchema: schema}})); listCollectionsOutput = db.runCommand({listCollections: 1, filter: {name: coll.getName()}}); assert.commandWorked(listCollectionsOutput); assert.eq(listCollectionsOutput.cursor.firstBatch[0].options.validator, {$jsonSchema: schema}); // Test that $jsonSchema and various internal match expressions work correctly with sibling // predicates. coll.drop(); assert.commandWorked(coll.insert({_id: 1, a: 1, b: 1})); assert.commandWorked(coll.insert({_id: 2, a: 2, b: 2})); assert.eq( 1, coll.find({$jsonSchema: {properties: {a: {type: "number"}}, required: ["a"]}, b: 1}).itcount()); assert.eq(1, coll.find({$or: [{$jsonSchema: {}, a: 1}, {b: 1}]}).itcount()); assert.eq(1, coll.find({$and: [{$jsonSchema: {}, a: 1}, {b: 1}]}).itcount()); if (!isSBEEnabled) { assert.eq(1, coll.find({$_internalSchemaMinProperties: 3, b: 2}).itcount()); assert.eq(1, coll.find({$_internalSchemaMaxProperties: 3, b: 2}).itcount()); } assert.eq(1, coll.find({$alwaysTrue: 1, b: 2}).itcount()); assert.eq(0, coll.find({$alwaysFalse: 1, b: 2}).itcount()); }());