summaryrefslogtreecommitdiff
path: root/src/mongo/db/matcher
diff options
context:
space:
mode:
authorDenis Grebennicov <denis.grebennicov@mongodb.com>2021-06-02 14:45:31 +0200
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2021-06-25 12:19:04 +0000
commit5100e995dfc1bd729d73e25b53007cfd3dc5c6f1 (patch)
tree8878224e63174fc55088fab3c1edced52159e2fe /src/mongo/db/matcher
parentf62b2b15033e1a65e154e5c86972ca2800c05bc4 (diff)
downloadmongo-5100e995dfc1bd729d73e25b53007cfd3dc5c6f1.tar.gz
SERVER-56207 Return JSON Schema `description` in document validation error message
Diffstat (limited to 'src/mongo/db/matcher')
-rw-r--r--src/mongo/db/matcher/doc_validation_error.cpp9
-rw-r--r--src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp196
-rw-r--r--src/mongo/db/matcher/doc_validation_util.cpp6
-rw-r--r--src/mongo/db/matcher/doc_validation_util.h3
-rw-r--r--src/mongo/db/matcher/expression.cpp25
-rw-r--r--src/mongo/db/matcher/expression.h31
-rw-r--r--src/mongo/db/matcher/schema/json_schema_parser.cpp19
7 files changed, 232 insertions, 57 deletions
diff --git a/src/mongo/db/matcher/doc_validation_error.cpp b/src/mongo/db/matcher/doc_validation_error.cpp
index 62fc0e72832..a577ab7576b 100644
--- a/src/mongo/db/matcher/doc_validation_error.cpp
+++ b/src/mongo/db/matcher/doc_validation_error.cpp
@@ -449,6 +449,10 @@ BSONObj toObjectWithPlaceholder(BSONElement element) {
return BSON(JSONSchemaParser::kNamePlaceholder << element);
}
+void appendSchemaAnnotations(const MatchExpression& expr, BSONObjBuilder& builder) {
+ expr.getErrorAnnotation()->schemaAnnotations.appendElements(builder);
+}
+
/**
* Append the error generated by one of 'expr's children to the current array builder of 'expr'
* if said child generated an error.
@@ -467,6 +471,7 @@ void finishLogicalOperatorChildError(const ListOfMatchExpression* expr,
BSONObjBuilder subBuilder = ctx->getCurrentArrayBuilder().subobjStart();
subBuilder.appendNumber("index",
static_cast<long long>(ctx->getCurrentChildIndex()));
+ appendSchemaAnnotations(*expr->getChild(ctx->getCurrentChildIndex()), subBuilder);
ctx->appendLatestCompleteError(&subBuilder);
subBuilder.done();
} else {
@@ -606,6 +611,7 @@ void generatePatternPropertyError(const InternalSchemaAllowedPropertiesMatchExpr
auto propertyName = element.fieldNameStringData().toString();
BSONObjBuilder patternBuilder;
patternBuilder.append("propertyName", propertyName);
+ appendSchemaAnnotations(*patternSchema.second->getFilter(), patternBuilder);
patternBuilder.append("regexMatched", patternSchema.first.rawRegex);
ctx->appendLatestCompleteError(&patternBuilder);
ctx->verifySizeAndAppend(patternBuilder.obj(), &ctx->getCurrentArrayBuilder());
@@ -637,6 +643,7 @@ void generateAdditionalPropertiesSchemaError(
invariant(firstFailingElement);
auto& builder = ctx->getCurrentObjBuilder();
builder.append("operatorName", "additionalProperties");
+ appendSchemaAnnotations(*expr.getChild(0), builder);
builder.append("reason", "at least one additional property did not match the subschema");
builder.append("failingProperty", firstFailingElement.fieldNameStringData().toString());
ctx->appendLatestCompleteError(&builder);
@@ -1746,6 +1753,7 @@ private:
if (auto attributeValue =
getValueForKeywordExpressionIfShouldGenerateError(*expr, {BSONType::Array})) {
appendOperatorName(*expr);
+ appendSchemaAnnotations(*expr->getChild(0), _context->getCurrentObjBuilder());
appendErrorReason(normalReason, invertedReason);
auto failingElement =
expr->findFirstMismatchInArray(attributeValue.embeddedObject(), nullptr);
@@ -2193,6 +2201,7 @@ public:
private:
void postVisitTreeOperator(const ListOfMatchExpression* expr,
const std::string& detailsString) {
+ appendSchemaAnnotations(*expr, _context->getCurrentObjBuilder());
finishLogicalOperatorChildError(expr, _context);
// If this node represents a 'properties' keyword or an individual property schema (denoted
// by '_property') and the current array builder has no elements, then this node will not
diff --git a/src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp b/src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp
index e5de9e08974..4c6bb91c6ac 100644
--- a/src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp
+++ b/src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp
@@ -34,6 +34,27 @@
namespace mongo {
namespace {
+// $jsonSchema
+TEST(JSONSchemaValidation, BasicJsonSchemaWithTitleAndDescription) {
+ BSONObj query = fromjson(
+ "{'$jsonSchema': {'properties': {'a': {'minimum': 1}},"
+ "title: 'example title', description: 'example description'}}");
+ BSONObj document = fromjson("{a: 0}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema',"
+ " 'title': 'example title',"
+ " 'description': 'example description',"
+ " 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'properties',"
+ " 'propertiesNotSatisfied': ["
+ " {'propertyName': 'a', 'details': "
+ " [{'operatorName': 'minimum',"
+ " 'specifiedAs': {'minimum' : 1},"
+ " 'reason': 'comparison failed',"
+ " 'consideredValue': 0}]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
// properties
TEST(JSONSchemaValidation, BasicProperties) {
BSONObj query = fromjson("{'$jsonSchema': {'properties': {'a': {'minimum': 1}}}}}");
@@ -53,13 +74,16 @@ TEST(JSONSchemaValidation, BasicProperties) {
// minimum
TEST(JSONSchemaValidation, MinimumNonNumericWithType) {
- BSONObj query =
- fromjson("{'$jsonSchema': {'properties': {'a': {'type': 'number','minimum': 1}}}}");
+ BSONObj query = fromjson(
+ "{'$jsonSchema': {'properties': {'a': {'type': 'number','minimum': 1,"
+ "title: 'property a', description: 'a >= 1'}}}}");
BSONObj document = fromjson("{'a': 'foo'}");
BSONObj expectedError = fromjson(
"{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
" {operatorName: 'properties', 'propertiesNotSatisfied': ["
- " {propertyName: 'a', 'details': ["
+ " {propertyName: 'a',"
+ " 'title': 'property a', 'description': 'a >= 1',"
+ " 'details': ["
" {'operatorName': 'type', "
" 'specifiedAs': { 'type': 'number' }, "
" 'reason': 'type did not match', "
@@ -134,13 +158,16 @@ TEST(JSONSchemaValidation, MinimumRequiredWithTypeAndScalarFailedMinimum) {
// maximum
TEST(JSONSchemaValidation, MaximumNonNumericWithType) {
- BSONObj query =
- fromjson("{'$jsonSchema': {'properties': {'a': {'type': 'number','maximum': 1}}}}");
+ BSONObj query = fromjson(
+ "{'$jsonSchema': {'properties': {'a': {'type': 'number','maximum': 1,"
+ "title: 'property a', description: 'a <= 1'}}}}");
BSONObj document = fromjson("{'a': 'foo'}");
BSONObj expectedError = fromjson(
"{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
" {operatorName: 'properties', 'propertiesNotSatisfied': ["
- " {propertyName: 'a', 'details': ["
+ " {propertyName: 'a',"
+ " 'title': 'property a', 'description': 'a <= 1',"
+ " 'details': ["
" {'operatorName': 'type', "
" 'specifiedAs': { 'type': 'number' }, "
" 'reason': 'type did not match', "
@@ -481,6 +508,7 @@ TEST(JSONSchemaValidation, MultipleNestedProperties) {
" 'consideredValue': 50}]}]}]}]}]}");
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
+
TEST(JSONSchemaValidation, JSONSchemaAndQueryOperators) {
BSONObj query = fromjson(
"{$and: ["
@@ -551,7 +579,7 @@ TEST(JSONSchemaValidation, MultipleTypeFailures) {
" {'properties':"
" {'a': {'type': 'string'}, "
" 'b': {'type': 'number'}, "
- " 'c': {'type': 'object'}}}}}");
+ " 'c': {'type': 'object', title: 'property c', description: 'c is a string'}}}}}");
BSONObj document = fromjson("{'a': {'b': 1}, 'b': 4, 'c': 'foo'}");
BSONObj expectedError = fromjson(
"{'operatorName': '$jsonSchema',"
@@ -563,7 +591,9 @@ TEST(JSONSchemaValidation, MultipleTypeFailures) {
" 'reason': 'type did not match',"
" 'consideredValue': {'b': 1},"
" 'consideredType': 'object'}]},"
- " {'propertyName': 'c', 'details': ["
+ " {'propertyName': 'c',"
+ " 'title': 'property c', 'description': 'c is a string',"
+ " 'details': ["
" {'operatorName': 'type',"
" 'specifiedAs': {'type': 'object'},"
" 'reason': 'type did not match',"
@@ -643,14 +673,17 @@ TEST(JSONSchemaValidation, BSONTypeNoImplicitArrayTraversal) {
BSONObj query = fromjson(
" {'$jsonSchema':"
" {'properties': "
- " {'a': {'bsonType': 'string'}}}}");
+ " {'a': {'bsonType': 'string',"
+ " title: 'property a', description: 'a is a string'}}}}");
// Even though 'a' is an array of strings, this is a type mismatch in the world of $jsonSchema.
BSONObj document = fromjson("{'a': ['Mihai', 'was', 'here']}");
BSONObj expectedError = fromjson(
"{'operatorName': '$jsonSchema',"
"'schemaRulesNotSatisfied': ["
" {'operatorName': 'properties', 'propertiesNotSatisfied': ["
- " {'propertyName': 'a', 'details': ["
+ " {'propertyName': 'a',"
+ " 'title': 'property a', 'description': 'a is a string',"
+ " 'details': ["
" {'operatorName': 'bsonType',"
" 'specifiedAs': {'bsonType': 'string'},"
" 'reason': 'type did not match',"
@@ -663,14 +696,17 @@ TEST(JSONSchemaValidation, BSONTypeNoImplicitArrayTraversal) {
// minLength
TEST(JSONSchemaValidation, BasicMinLength) {
- BSONObj query =
- fromjson("{'$jsonSchema': {'properties': {'a': {'type': 'string','minLength': 4}}}}");
+ BSONObj query = fromjson(
+ "{'$jsonSchema': {'properties': {'a': {'type': 'string', 'minLength': 4,"
+ "title: 'property a', description: 'a min length is 4'}}}}");
BSONObj document = fromjson("{'a': 'foo'}");
BSONObj expectedError = fromjson(
"{'operatorName': '$jsonSchema',"
"'schemaRulesNotSatisfied': ["
" {'operatorName': 'properties', 'propertiesNotSatisfied': ["
- " {'propertyName': 'a', 'details': ["
+ " {'propertyName': 'a',"
+ " 'title': 'property a', 'description': 'a min length is 4',"
+ " 'details': ["
" {'operatorName': 'minLength',"
" 'specifiedAs': {'minLength': 4},"
" 'reason': 'specified string length was not satisfied',"
@@ -822,14 +858,17 @@ TEST(JSONSchemaValidation, MinLengthInvertedMissingProperty) {
// maxLength
TEST(JSONSchemaValidation, BasicMaxLength) {
- BSONObj query =
- fromjson("{'$jsonSchema': {'properties': {'a': {'type': 'string','maxLength': 4}}}}");
+ BSONObj query = fromjson(
+ "{'$jsonSchema': {'properties': {'a': {'type': 'string','maxLength': 4,"
+ "title: 'property a', description: 'a max length is 4'}}}}");
BSONObj document = fromjson("{'a': 'foo, bar, baz'}");
BSONObj expectedError = fromjson(
"{'operatorName': '$jsonSchema',"
"'schemaRulesNotSatisfied': ["
" {'operatorName': 'properties', 'propertiesNotSatisfied': ["
- " {'propertyName': 'a', 'details': ["
+ " {'propertyName': 'a',"
+ " 'title': 'property a', 'description': 'a max length is 4',"
+ " 'details': ["
" {'operatorName': 'maxLength',"
" 'specifiedAs': {'maxLength': 4},"
" 'reason': 'specified string length was not satisfied',"
@@ -1076,7 +1115,9 @@ TEST(JSONSchemaValidation, PatternNested) {
"{'$jsonSchema': {"
" 'properties': {"
" 'a': {'properties': "
- " {'b': {'type': 'string', 'pattern': '^S'}}}}}}}}");
+ " {'b': {'type': 'string', 'pattern': '^S',"
+ " title: 'pattern property',"
+ " description: 'values of a should start with S'}}}}}}}}");
BSONObj document = fromjson("{'a': {'b': 'foo'}}");
BSONObj expectedError = fromjson(
"{'operatorName': '$jsonSchema',"
@@ -1088,6 +1129,8 @@ TEST(JSONSchemaValidation, PatternNested) {
" [{'operatorName': 'properties',"
" 'propertiesNotSatisfied': ["
" {'propertyName': 'b', "
+ " 'title': 'pattern property',"
+ " 'description': 'values of a should start with S',"
" 'details': ["
" {'operatorName': 'pattern',"
" 'specifiedAs': {'pattern': '^S'},"
@@ -1247,8 +1290,8 @@ TEST(JSONSchemaValidation, MultipleOfNested) {
" 'details': ["
" {'operatorName': 'multipleOf',"
" 'specifiedAs': {'multipleOf': 2.1},"
- " 'reason': 'considered value is not a multiple of "
- "the specified value',"
+ " 'reason': 'considered value is not a multiple of the "
+ "specified value',"
" 'consideredValue': 1}]}]}]}]}]}");
doc_validation_error::verifyGeneratedError(query, document, expectedError);
doc_validation_error::verifyGeneratedError(query, document, expectedError);
@@ -1290,6 +1333,7 @@ TEST(JSONSchemaValidation, MultipleOfInvertedMissingProperty) {
" {'operatorName': 'not', 'reason': 'child expression matched'}]}");
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
+
// encrypt
TEST(JSONSchemaValidation, BasicEncrypt) {
BSONObj query =
@@ -1396,6 +1440,7 @@ TEST(JSONSchemaValidation, EncryptWithSubtypeInvertedMissingProperty) {
" {'index': 0, 'details':{'operatorName': '$jsonSchema', 'reason': 'schema matched'}}]}");
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
+
// Logical keywords
// allOf
@@ -1473,6 +1518,31 @@ TEST(JSONSchemaLogicalKeywordValidation, TopLevelAnyOf) {
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
+TEST(JSONSchemaLogicalKeywordValidation, NestedAnyOfWithDescription) {
+ BSONObj query = fromjson(
+ "{$jsonSchema: {properties: {a: { description: 'property a',"
+ "anyOf: [{bsonType: 'number', description: 'number?'}, {bsonType: 'string'}]}}}}");
+ BSONObj document = fromjson("{a: {}}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', schemaRulesNotSatisfied: ["
+ " {'operatorName': 'properties', propertiesNotSatisfied: ["
+ " {propertyName: 'a', description: 'property a', details: [ "
+ " {'operatorName': 'anyOf', schemasNotSatisfied: ["
+ " {index: 0, description: 'number?', details: [{"
+ " operatorName: 'bsonType',"
+ " specifiedAs: { bsonType: 'number' },"
+ " reason: 'type did not match',"
+ " consideredValue: {},"
+ " consideredType: 'object' }]},"
+ " {index: 1, details: [{"
+ " operatorName: 'bsonType',"
+ " specifiedAs: { bsonType: 'string' },"
+ " reason: 'type did not match',"
+ " consideredValue: {},"
+ " consideredType: 'object' }]}]}]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
TEST(JSONSchemaLogicalKeywordValidation, NestedAnyOf) {
BSONObj query =
fromjson("{$jsonSchema: {properties: {a: {anyOf: [{type: 'string'},{maximum: 3}]}}}}");
@@ -1582,13 +1652,15 @@ TEST(JSONSchemaLogicalKeywordValidation, NotOverAnyOf) {
// oneOf
TEST(JSONSchemaLogicalKeywordValidation, OneOfMoreThanOneMatchingClause) {
- BSONObj query =
- fromjson("{$jsonSchema: {properties: {a: {oneOf: [{minimum: 1},{maximum: 3}]}}}}");
+ BSONObj query = fromjson(
+ "{$jsonSchema: {properties: {a: {oneOf: [{minimum: 1},{maximum: 3}],"
+ "description: 'oneOf description'}}}}");
BSONObj document = fromjson("{a: 2}");
BSONObj expectedError = fromjson(
"{'operatorName': '$jsonSchema',schemaRulesNotSatisfied: ["
" {'operatorName': 'properties', propertiesNotSatisfied: ["
- " {propertyName: 'a', 'details': ["
+ " {propertyName: 'a', description: 'oneOf description',"
+ " 'details': ["
" {'operatorName': 'oneOf', "
" 'reason': 'more than one subschema matched', "
" 'matchingSchemaIndexes': [0, 1]}]}]}]}");
@@ -1667,6 +1739,21 @@ TEST(JSONSchemaLogicalKeywordValidation, BasicNot) {
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
+TEST(JSONSchemaLogicalKeywordValidation, BasicNotWithDescription) {
+ BSONObj query = fromjson(
+ "{$jsonSchema: { properties: { a: { not: { type: 'number', title: 'type title'},"
+ " title: 'not title' } } } }");
+ BSONObj document = fromjson("{a: 1}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'properties', 'propertiesNotSatisfied': ["
+ " {propertyName: 'a',"
+ " title: 'not title',"
+ " details: ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
TEST(JSONSchemaLogicalKeywordValidation, NestedNot) {
BSONObj query = fromjson("{$jsonSchema: {not: {not: {properties: {a: {minimum: 3}}}}}}");
BSONObj document = fromjson("{a: 1}");
@@ -2072,16 +2159,19 @@ TEST(JSONSchemaValidation, ArrayItemsSingleSchema) {
BSONObj query = fromjson(
" {'$jsonSchema':"
" {'properties': "
- " {'a': {'items': {'type': 'string'}}}}}");
+ " {'a': {'items': {'type': 'string', 'description': 'elements must be of string "
+ "type'}}}}}");
BSONObj document = fromjson("{'a': [1, 'A', {}]}");
BSONObj expectedError = fromjson(
"{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
" {'operatorName': 'properties', 'propertiesNotSatisfied': ["
" {'propertyName': 'a', 'details': ["
- " {'operatorName': 'items', 'reason': 'At least one item did not match the "
- "sub-schema', 'itemIndex': 0, 'details': ["
+ " {'operatorName': 'items',"
+ " 'description': 'elements must be of string type',"
+ " 'reason': 'At least one item did not match the sub-schema',"
+ " 'itemIndex': 0, 'details': ["
" {'operatorName': 'type', 'specifiedAs': {'type': 'string'}, "
- "'reason': 'type did not match', 'consideredValue': 1, 'consideredType': 'int'}]}]}]}]}");
+ "'reason': 'type did not match', 'consideredValue': 1, 'consideredType':'int'}]}]}]}]}");
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
@@ -2252,7 +2342,8 @@ TEST(JSONSchemaValidation, ArrayItemsSchemaArray) {
" {'operatorName': 'items', 'details': ["
" {'itemIndex': 1, 'details': ["
" {'operatorName': 'type', 'specifiedAs': {'type': 'string'}, 'reason': 'type did "
- "not match', 'consideredValue': 2, 'consideredType': 'int'}]}"
+ "not match',"
+ " 'consideredValue': 2, 'consideredType': 'int'}]}"
"]}]}]}]}");
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
@@ -2395,16 +2486,19 @@ TEST(JSONSchemaValidation, ArrayAdditionalItemsSchema) {
" {'$jsonSchema':"
" {'properties': "
" {'a': {'type': 'array', 'items': [{'type': 'number'}, {'type': 'string'}], "
- "'additionalItems': {'type': 'object'}}}}}");
+ "'additionalItems': {'type': 'object', 'description': 'only extra documents'}}}}}");
BSONObj document = fromjson("{'a': [1, 'First', {}, 'Extra element']}");
BSONObj expectedError = fromjson(
"{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
" {'operatorName': 'properties', 'propertiesNotSatisfied': ["
" {'propertyName': 'a', 'details': ["
- " {'operatorName': 'additionalItems', 'reason': 'At least one additional item did not "
+ " {'operatorName': 'additionalItems',"
+ " 'description': 'only extra documents',"
+ " 'reason': 'At least one additional item did not "
"match the sub-schema', 'itemIndex': 3, 'details': ["
" {'operatorName': 'type', 'specifiedAs': {'type': 'object'}, 'reason': 'type did "
- "not match', 'consideredValue': 'Extra element', 'consideredType': 'string'}]}]}]}]}");
+ "not match',"
+ " 'consideredValue': 'Extra element', 'consideredType': 'string'}]}]}]}]}");
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
@@ -2606,6 +2700,7 @@ TEST(JSONSchemaValidation, ArrayAdditionalItemsFalseAlwaysTrue) {
" {'operatorName': '$jsonSchema', 'reason': 'schema matched'}}]}");
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
+
// Object keywords
// minProperties
@@ -2738,11 +2833,13 @@ TEST(JSONSchemaValidation, NestedMaxPropertiesTypeMismatch) {
// property dependencies
TEST(JSONSchemaValidation, BasicPropertyDependency) {
- BSONObj query = fromjson("{'$jsonSchema': {'dependencies': {'a': ['b', 'c']}}}");
+ BSONObj query = fromjson(
+ "{'$jsonSchema': {'dependencies': {'a': ['b', 'c'],"
+ " title: 'a needs b and c'}}}");
BSONObj document = fromjson("{'a': 1, 'b': 2}");
BSONObj expectedError = fromjson(
"{operatorName: '$jsonSchema', schemaRulesNotSatisfied: ["
- " {operatorName: 'dependencies', failingDependencies: ["
+ " {operatorName: 'dependencies', title: 'a needs b and c', failingDependencies: ["
" {conditionalProperty: 'a', "
" missingProperties: [ 'c' ]}]}]}");
doc_validation_error::verifyGeneratedError(query, document, expectedError);
@@ -2750,7 +2847,7 @@ TEST(JSONSchemaValidation, BasicPropertyDependency) {
TEST(JSONSchemaValidation, NestedPropertyDependency) {
BSONObj query =
- fromjson("{'$jsonSchema': {'properties': {'obj': {'dependencies': {'a': ['b', 'c']}}}}}");
+ fromjson("{'$jsonSchema': {'properties': {'obj': {'dependencies': {'a': ['b','c']}}}}}");
BSONObj document = fromjson("{'obj': {'a': 1, 'b': 2}}");
BSONObj expectedError = fromjson(
"{operatorName: '$jsonSchema', schemaRulesNotSatisfied: ["
@@ -2827,11 +2924,12 @@ TEST(JSONSchemaValidation, PropertyDependencyWithRequiredMissingDependency) {
// schema dependencies
TEST(JSONSchemaValidation, BasicSchemaDependency) {
BSONObj query = fromjson(
- "{'$jsonSchema': {'dependencies': {'a': {'properties': {'b': {'type': 'number'}}}}}}");
+ "{'$jsonSchema': {'dependencies': {'a': {'properties': {'b': {'type': 'number'}}},"
+ " title: 'a needs b'}}}");
BSONObj document = fromjson("{'a': 1, 'b': 'foo'}");
BSONObj expectedError = fromjson(
"{operatorName: '$jsonSchema', schemaRulesNotSatisfied: ["
- " {operatorName: 'dependencies', failingDependencies: ["
+ " {operatorName: 'dependencies', title: 'a needs b', failingDependencies: ["
" {conditionalProperty: 'a', details: ["
" {operatorName: 'properties',propertiesNotSatisfied: ["
" {propertyName: 'b', details: ["
@@ -2867,7 +2965,7 @@ TEST(JSONSchemaValidation, NestedSchemaDependency) {
TEST(JSONSchemaValidation, SchemaDependencyWithRequiredMissingProperty) {
BSONObj query = fromjson(
- "{'$jsonSchema': {'required': ['a'], 'dependencies': {'a': {'properties': {'b': {'type': "
+ "{'$jsonSchema': {'required': ['a'], 'dependencies': {'a': {'properties': {'b': {'type':"
"'number'}}}}}}");
BSONObj document = fromjson("{'b': 'foo'}");
BSONObj expectedError = fromjson(
@@ -3384,14 +3482,18 @@ TEST(JSONSchemaValidation, PatternPropertiesAndAdditionalPropertiesFalseNeitherF
TEST(JSONSchemaValidation, PatternPropertiesAndAdditionalPropertiesSchema) {
BSONObj query = fromjson(
"{'$jsonSchema': "
- " {'patternProperties': {'^S': {'type': 'number'}}, "
- " 'additionalProperties': {'type': 'string'}}}}");
+ " {'patternProperties': {'^S': {'type': 'number', title: 'properties starting with S',"
+ " description: 'property should be of integer type'}},"
+ " 'additionalProperties': {'type': 'string', title: 'additional properties',"
+ " description: 'additional properties are strings'}}}}");
BSONObj document = fromjson("{'Super': 1, 'Slow': 'oh no a string', b: 1}");
BSONObj expectedError = fromjson(
"{operatorName: '$jsonSchema', schemaRulesNotSatisfied: ["
" {operatorName: 'additionalProperties', "
- " reason: 'at least one additional property did not match the subschema',"
- " failingProperty: 'b', details: ["
+ " title: 'additional properties',"
+ " description: 'additional properties are strings',"
+ " reason: 'at least one additional property did not match the subschema',"
+ " failingProperty: 'b', details: ["
" {operatorName: 'type', "
" specifiedAs: {type: 'string'}, "
" reason: 'type did not match', "
@@ -3399,13 +3501,15 @@ TEST(JSONSchemaValidation, PatternPropertiesAndAdditionalPropertiesSchema) {
" consideredType: 'int'}]}, "
" {operatorName: 'patternProperties', details: ["
" {propertyName: 'Slow',"
- " regexMatched: '^S',"
- " details: ["
+ " title: 'properties starting with S',"
+ " description: 'property should be of integer type',"
+ " regexMatched: '^S',"
+ " details: ["
" {operatorName: 'type',"
- " specifiedAs: {type:'number'},"
- " reason: 'type did not match',"
- " consideredValue: 'oh no a string',"
- " consideredType: 'string'}]}]}]}");
+ " specifiedAs: {type:'number'},"
+ " reason: 'type did not match',"
+ " consideredValue: 'oh no a string',"
+ " consideredType: 'string'}]}]}]}");
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
diff --git a/src/mongo/db/matcher/doc_validation_util.cpp b/src/mongo/db/matcher/doc_validation_util.cpp
index d2fdd19e0fc..4689ce0e576 100644
--- a/src/mongo/db/matcher/doc_validation_util.cpp
+++ b/src/mongo/db/matcher/doc_validation_util.cpp
@@ -33,9 +33,11 @@ namespace mongo::doc_validation_error {
std::unique_ptr<MatchExpression::ErrorAnnotation> createAnnotation(
const boost::intrusive_ptr<ExpressionContext>& expCtx,
const std::string& tag,
- BSONObj annotation) {
+ BSONObj annotation,
+ const BSONObj& jsonSchemaElement) {
if (expCtx->isParsingCollectionValidator) {
- return std::make_unique<MatchExpression::ErrorAnnotation>(tag, std::move(annotation));
+ return std::make_unique<MatchExpression::ErrorAnnotation>(
+ tag, std::move(annotation), jsonSchemaElement);
} else {
return nullptr;
}
diff --git a/src/mongo/db/matcher/doc_validation_util.h b/src/mongo/db/matcher/doc_validation_util.h
index 0e5852349fd..687a21a5065 100644
--- a/src/mongo/db/matcher/doc_validation_util.h
+++ b/src/mongo/db/matcher/doc_validation_util.h
@@ -41,7 +41,8 @@ namespace mongo::doc_validation_error {
std::unique_ptr<MatchExpression::ErrorAnnotation> createAnnotation(
const boost::intrusive_ptr<ExpressionContext>& expCtx,
const std::string& tag,
- BSONObj annotation);
+ BSONObj annotation,
+ const BSONObj& jsonSchemaElement = BSONObj());
std::unique_ptr<MatchExpression::ErrorAnnotation> createAnnotation(
const boost::intrusive_ptr<ExpressionContext>& expCtx,
diff --git a/src/mongo/db/matcher/expression.cpp b/src/mongo/db/matcher/expression.cpp
index 0057f0c2903..5f2324eff3f 100644
--- a/src/mongo/db/matcher/expression.cpp
+++ b/src/mongo/db/matcher/expression.cpp
@@ -31,6 +31,7 @@
#include "mongo/bson/bsonmisc.h"
#include "mongo/bson/bsonobj.h"
+#include "mongo/db/matcher/schema/json_schema_parser.h"
namespace mongo {
@@ -163,4 +164,28 @@ void MatchExpression::addDependencies(DepsTracker* deps) const {
_doAddDependencies(deps);
}
+
+MatchExpression::ErrorAnnotation::SchemaAnnotations::SchemaAnnotations(
+ const BSONObj& jsonSchemaElement) {
+ auto title = jsonSchemaElement[JSONSchemaParser::kSchemaTitleKeyword];
+ if (title.type() == BSONType::String) {
+ this->title = {title.String()};
+ }
+
+ auto description = jsonSchemaElement[JSONSchemaParser::kSchemaDescriptionKeyword];
+ if (description.type() == BSONType::String) {
+ this->description = {description.String()};
+ }
+}
+
+void MatchExpression::ErrorAnnotation::SchemaAnnotations::appendElements(
+ BSONObjBuilder& builder) const {
+ if (title) {
+ builder << JSONSchemaParser::kSchemaTitleKeyword << title.get();
+ }
+
+ if (description) {
+ builder << JSONSchemaParser::kSchemaDescriptionKeyword << description.get();
+ }
+}
} // namespace mongo
diff --git a/src/mongo/db/matcher/expression.h b/src/mongo/db/matcher/expression.h
index 6d8a0d6eab5..41f99fe65d4 100644
--- a/src/mongo/db/matcher/expression.h
+++ b/src/mongo/db/matcher/expression.h
@@ -229,17 +229,41 @@ public:
};
/**
+ * JSON Schema annotations - 'title' and 'description' attributes.
+ */
+ struct SchemaAnnotations {
+ /**
+ * Constructs JSON schema annotations with annotation fields not set.
+ */
+ SchemaAnnotations() {}
+
+ /**
+ * Constructs JSON schema annotations from JSON Schema element 'jsonSchemaElement'.
+ */
+ SchemaAnnotations(const BSONObj& jsonSchemaElement);
+
+ void appendElements(BSONObjBuilder& builder) const;
+
+ boost::optional<std::string> title;
+ boost::optional<std::string> description;
+ };
+
+ /**
* Constructs an annotation for a MatchExpression which does not contribute to error output.
*/
- ErrorAnnotation(Mode mode) : tag(""), annotation(BSONObj()), mode(mode) {
+ ErrorAnnotation(Mode mode)
+ : tag(""), annotation(BSONObj()), mode(mode), schemaAnnotations(SchemaAnnotations()) {
invariant(mode != Mode::kGenerateError);
}
/**
* Constructs a complete annotation for a MatchExpression which contributes to error output.
*/
- ErrorAnnotation(std::string tag, BSONObj annotation)
- : tag(std::move(tag)), annotation(annotation.getOwned()), mode(Mode::kGenerateError) {}
+ ErrorAnnotation(std::string tag, BSONObj annotation, BSONObj schemaAnnotationsObj)
+ : tag(std::move(tag)),
+ annotation(annotation.getOwned()),
+ mode(Mode::kGenerateError),
+ schemaAnnotations(SchemaAnnotations(schemaAnnotationsObj)) {}
std::unique_ptr<ErrorAnnotation> clone() const {
return std::make_unique<ErrorAnnotation>(*this);
@@ -252,6 +276,7 @@ public:
// Tracks the original expression as specified by the user.
const BSONObj annotation;
const Mode mode;
+ const SchemaAnnotations schemaAnnotations;
};
/**
diff --git a/src/mongo/db/matcher/schema/json_schema_parser.cpp b/src/mongo/db/matcher/schema/json_schema_parser.cpp
index 4aa7e67f343..f3c601b4329 100644
--- a/src/mongo/db/matcher/schema/json_schema_parser.cpp
+++ b/src/mongo/db/matcher/schema/json_schema_parser.cpp
@@ -564,7 +564,8 @@ StatusWithMatchExpression parseProperties(const boost::intrusive_ptr<ExpressionC
nestedSchemaMatch.getValue()->setErrorAnnotation(doc_validation_error::createAnnotation(
expCtx,
"_property",
- BSON("propertyName" << property.fieldNameStringData().toString())));
+ BSON("propertyName" << property.fieldNameStringData().toString()),
+ property.Obj()));
if (requiredProperties.find(property.fieldNameStringData()) != requiredProperties.end()) {
// The field name for which we created the nested schema is a required property. This
// property must exist and therefore must match 'nestedSchemaMatch'.
@@ -927,9 +928,17 @@ StatusWithMatchExpression parseDependencies(const boost::intrusive_ptr<Expressio
}
auto andExpr = std::make_unique<AndMatchExpression>(doc_validation_error::createAnnotation(
- expCtx, dependencies.fieldNameStringData().toString(), BSONObj()));
+ expCtx, dependencies.fieldNameStringData().toString(), BSONObj(), dependencies.Obj()));
for (auto&& dependency : dependencies.embeddedObject()) {
if (dependency.type() != BSONType::Object && dependency.type() != BSONType::Array) {
+ // Allow JSON Schema annotations under "dependency" keyword.
+ const auto isSchemaAnnotation =
+ dependency.fieldNameStringData() == JSONSchemaParser::kSchemaTitleKeyword ||
+ dependency.fieldNameStringData() == JSONSchemaParser::kSchemaDescriptionKeyword;
+ if (dependency.type() == BSONType::String && isSchemaAnnotation) {
+ continue;
+ }
+
return {ErrorCodes::TypeMismatch,
str::stream() << "property '" << dependency.fieldNameStringData()
<< "' in $jsonSchema keyword '"
@@ -1748,7 +1757,7 @@ StatusWithMatchExpression _parse(const boost::intrusive_ptr<ExpressionContext>&
// to '$jsonSchema', the caller is responsible for providing this information by overwriting
// this annotation.
auto andExpr = std::make_unique<AndMatchExpression>(
- doc_validation_error::createAnnotation(expCtx, "_subschema", BSONObj()));
+ doc_validation_error::createAnnotation(expCtx, "_subschema", BSONObj(), schema));
auto translationStatus =
translateScalarKeywords(expCtx, keywordMap, path, typeExpr.get(), andExpr.get());
@@ -1874,8 +1883,8 @@ StatusWithMatchExpression JSONSchemaParser::parse(
if (translation.isOK()) {
if (auto topLevelAnnotation = translation.getValue()->getErrorAnnotation()) {
auto oldAnnotation = topLevelAnnotation->annotation;
- translation.getValue()->setErrorAnnotation(
- doc_validation_error::createAnnotation(expCtx, "$jsonSchema", oldAnnotation));
+ translation.getValue()->setErrorAnnotation(doc_validation_error::createAnnotation(
+ expCtx, "$jsonSchema", oldAnnotation, schema));
}
}
expCtx->sbeCompatible = false;