diff options
author | Denis Grebennicov <denis.grebennicov@mongodb.com> | 2021-06-02 14:45:31 +0200 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2021-06-25 12:19:04 +0000 |
commit | 5100e995dfc1bd729d73e25b53007cfd3dc5c6f1 (patch) | |
tree | 8878224e63174fc55088fab3c1edced52159e2fe /src/mongo/db/matcher | |
parent | f62b2b15033e1a65e154e5c86972ca2800c05bc4 (diff) | |
download | mongo-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.cpp | 9 | ||||
-rw-r--r-- | src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp | 196 | ||||
-rw-r--r-- | src/mongo/db/matcher/doc_validation_util.cpp | 6 | ||||
-rw-r--r-- | src/mongo/db/matcher/doc_validation_util.h | 3 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression.cpp | 25 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression.h | 31 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/json_schema_parser.cpp | 19 |
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; |