Skip to content

Commit dad85ca

Browse files
author
Steve Hanson
authored
Merge pull request Tencent#2 from smhdfdl/multiple-validation-failures-and-validation-messages
Multiple validation failures and validation messages
2 parents 5d17b24 + 9bb81e2 commit dad85ca

File tree

10 files changed

+1278
-234
lines changed

10 files changed

+1278
-234
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
!/bin/encodings
44
!/bin/jsonchecker
55
!/bin/types
6+
!/bin/unittestschema
67
/build
78
/doc/html
89
/doc/doxygen_*.db

bin/unittestschema/address.json

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"version": {
5+
"$ref": "#/definitions/decimal_type"
6+
},
7+
"address": {
8+
"$ref": "#/definitions/address_type"
9+
},
10+
"phones": {
11+
"type": "array",
12+
"minItems": 1,
13+
"maxItems": 2,
14+
"uniqueItems": true,
15+
"items": {
16+
"$ref": "#/definitions/phone_type"
17+
}
18+
},
19+
"names": {
20+
"type": "array",
21+
"items": [
22+
{ "type": "string" },
23+
{ "type": "string" }
24+
],
25+
"additionalItems": false
26+
},
27+
"extra": {
28+
"type": "object",
29+
"patternProperties": {
30+
"^S_": { "type": "string" }
31+
}
32+
},
33+
"gender": {
34+
"type": "string",
35+
"enum": ["M", "F"]
36+
}
37+
},
38+
"additionalProperties": false,
39+
"dependencies": {
40+
"address": [ "version" ],
41+
"names": {
42+
"properties": {
43+
"version": { "$ref": "#/definitions/decimal_type" }
44+
},
45+
"required": ["version"]
46+
}
47+
},
48+
"definitions": {
49+
"address_type": {
50+
"type": "object",
51+
"properties": {
52+
"number": {
53+
"$ref": "#/definitions/positiveInt_type"
54+
},
55+
"street1": {
56+
"type": "string"
57+
},
58+
"street2": {
59+
"type": ["string", "null"]
60+
},
61+
"street3": {
62+
"not": { "type": ["boolean", "number", ",integer", "object", "null"] }
63+
},
64+
"city": {
65+
"type": "string",
66+
"maxLength": 10,
67+
"minLength": 4
68+
},
69+
"area": {
70+
"oneOf": [
71+
{ "$ref": "#/definitions/county_type" },
72+
{ "$ref": "#/definitions/province_type" }
73+
]
74+
},
75+
"country": {
76+
"allOf": [
77+
{ "$ref": "#/definitions/country_type" }
78+
]
79+
},
80+
"postcode": {
81+
"anyOf": [
82+
{ "type": "string", "pattern": "^[A-Z]{2}[0-9]{1,2} [0-9][A-Z]{2}$" },
83+
{ "type": "string", "pattern": "^[0-9]{5}$" }
84+
]
85+
}
86+
},
87+
"minProperties": 7,
88+
"required": [
89+
"number",
90+
"street1",
91+
"city"
92+
]
93+
},
94+
"country_type": {
95+
"type": "string",
96+
"enum": ["UK", "Canada"]
97+
},
98+
"county_type": {
99+
"type": "string",
100+
"enum": ["Sussex", "Surrey", "Kent"]
101+
},
102+
"province_type": {
103+
"type": "string",
104+
"enum": ["Quebec", "BC", "Alberta"]
105+
},
106+
"date_type": {
107+
"pattern": "^([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?$",
108+
"type": "string"
109+
},
110+
"positiveInt_type": {
111+
"minimum": 0,
112+
"exclusiveMinimum": true,
113+
"maximum": 100,
114+
"exclusiveMaximum": true,
115+
"type": "integer"
116+
},
117+
"decimal_type": {
118+
"multipleOf": 1.0,
119+
"type": "number"
120+
},
121+
"time_type": {
122+
"pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?$",
123+
"type": "string"
124+
},
125+
"unsignedInt_type": {
126+
"type": "integer",
127+
"minimum": 0,
128+
"maximum": 99999
129+
},
130+
"phone_type": {
131+
"pattern": "^[0-9]*-[0-9]*",
132+
"type": "string"
133+
},
134+
"url_type": {
135+
"pattern": "^\\S*$",
136+
"type": "string"
137+
}
138+
}
139+
}

bin/unittestschema/allOf_address.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"allOf": [
3+
{
4+
"$ref": "http://localhost:1234/address.json#"
5+
}
6+
]
7+
}

bin/unittestschema/anyOf_address.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"anyOf": [
3+
{
4+
"$ref": "http://localhost:1234/address.json#"
5+
}
6+
]
7+
}

bin/unittestschema/oneOf_address.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"oneOf": [
3+
{
4+
"$ref": "http://localhost:1234/address.json#"
5+
}
6+
]
7+
}

example/schemavalidator/schemavalidator.cpp

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,124 @@
77
#include "rapidjson/schema.h"
88
#include "rapidjson/stringbuffer.h"
99
#include "rapidjson/prettywriter.h"
10+
#include <string>
11+
#include <iostream>
12+
#include <sstream>
1013

1114
using namespace rapidjson;
1215

16+
typedef GenericValue<UTF8<>, CrtAllocator > ValueType;
17+
18+
// Forward ref
19+
static void CreateErrorMessages(const ValueType& errors, size_t depth, const char* context);
20+
21+
// Convert GenericValue to std::string
22+
static std::string GetString(const ValueType& val) {
23+
std::ostringstream s;
24+
if (val.IsString())
25+
s << val.GetString();
26+
else if (val.IsDouble())
27+
s << val.GetDouble();
28+
else if (val.IsUint())
29+
s << val.GetUint();
30+
else if (val.IsInt())
31+
s << val.GetInt();
32+
else if (val.IsUint64())
33+
s << val.GetUint64();
34+
else if (val.IsInt64())
35+
s << val.GetInt64();
36+
else if (val.IsBool() && val.GetBool())
37+
s << "true";
38+
else if (val.IsBool())
39+
s << "false";
40+
else if (val.IsFloat())
41+
s << val.GetFloat();
42+
return s.str();}
43+
44+
// Create the error message for a named error
45+
// The error object can either be empty or contain at least member properties:
46+
// {"errorCode": <code>, "instanceRef": "<pointer>", "schemaRef": "<pointer>" }
47+
// Additional properties may be present for use as inserts.
48+
// An "errors" property may be present if there are child errors.
49+
static void HandleError(const char* errorName, const ValueType& error, size_t depth, const char* context) {
50+
if (!error.ObjectEmpty()) {
51+
// Get error code and look up error message text (English)
52+
int code = error["errorCode"].GetInt();
53+
std::string message(GetValidateError_En(static_cast<ValidateErrorCode>(code)));
54+
// For each member property in the error, see if its name exists as an insert in the error message and if so replace with the stringified property value
55+
// So for example - "Number '%actual' is not a multiple of the 'multipleOf' value '%expected'." - we would expect "actual" and "expected" members.
56+
for (ValueType::ConstMemberIterator insertsItr = error.MemberBegin();
57+
insertsItr != error.MemberEnd(); ++insertsItr) {
58+
std::string insertName("%");
59+
insertName += insertsItr->name.GetString(); // eg "%actual"
60+
size_t insertPos = message.find(insertName);
61+
if (insertPos != std::string::npos) {
62+
std::string insertString("");
63+
const ValueType &insert = insertsItr->value;
64+
if (insert.IsArray()) {
65+
// Member is an array so create comma-separated list of items for the insert string
66+
for (ValueType::ConstValueIterator itemsItr = insert.Begin(); itemsItr != insert.End(); ++itemsItr) {
67+
if (itemsItr != insert.Begin()) insertString += ",";
68+
insertString += GetString(*itemsItr);
69+
}
70+
} else {
71+
insertString += GetString(insert);
72+
}
73+
message.replace(insertPos, insertName.length(), insertString);
74+
}
75+
}
76+
// Output error message, references, context
77+
std::string indent(depth * 2, ' ');
78+
std::cout << indent << "Error Name: " << errorName << std::endl;
79+
std::cout << indent << "Message: " << message.c_str() << std::endl;
80+
std::cout << indent << "Instance: " << error["instanceRef"].GetString() << std::endl;
81+
std::cout << indent << "Schema: " << error["schemaRef"].GetString() << std::endl;
82+
if (depth > 0) std::cout << indent << "Context: " << context << std::endl;
83+
std::cout << std::endl;
84+
85+
// If child errors exist, apply the process recursively to each error structure.
86+
// This occurs for "oneOf", "allOf", "anyOf" and "dependencies" errors, so pass the error name as context.
87+
if (error.HasMember("errors")) {
88+
depth++;
89+
const ValueType &childErrors = error["errors"];
90+
if (childErrors.IsArray()) {
91+
// Array - each item is an error structure - example
92+
// "anyOf": {"errorCode": ..., "errors":[{"pattern": {"errorCode\": ...\"}}, {"pattern": {"errorCode\": ...}}]
93+
for (ValueType::ConstValueIterator errorsItr = childErrors.Begin();
94+
errorsItr != childErrors.End(); ++errorsItr) {
95+
CreateErrorMessages(*errorsItr, depth, errorName);
96+
}
97+
} else if (childErrors.IsObject()) {
98+
// Object - each member is an error structure - example
99+
// "dependencies": {"errorCode": ..., "errors": {"address": {"required": {"errorCode": ...}}, "name": {"required": {"errorCode": ...}}}
100+
for (ValueType::ConstMemberIterator propsItr = childErrors.MemberBegin();
101+
propsItr != childErrors.MemberEnd(); ++propsItr) {
102+
CreateErrorMessages(propsItr->value, depth, errorName);
103+
}
104+
}
105+
}
106+
}
107+
}
108+
109+
// Create error message for all errors in an error structure
110+
// Context is used to indicate whether the error structure has a parent 'dependencies', 'allOf', 'anyOf' or 'oneOf' error
111+
static void CreateErrorMessages(const ValueType& errors, size_t depth = 0, const char* context = 0) {
112+
// Each member property contains one or more errors of a given type
113+
for (ValueType::ConstMemberIterator errorTypeItr = errors.MemberBegin(); errorTypeItr != errors.MemberEnd(); ++errorTypeItr) {
114+
const char* errorName = errorTypeItr->name.GetString();
115+
const ValueType& errorContent = errorTypeItr->value;
116+
if (errorContent.IsArray()) {
117+
// Member is an array where each item is an error - eg "type": [{"errorCode": ...}, {"errorCode": ...}]
118+
for (ValueType::ConstValueIterator contentItr = errorContent.Begin(); contentItr != errorContent.End(); ++contentItr) {
119+
HandleError(errorName, *contentItr, depth, context);
120+
}
121+
} else if (errorContent.IsObject()) {
122+
// Member is an object which is a single error - eg "type": {"errorCode": ... }
123+
HandleError(errorName, errorContent, depth, context);
124+
}
125+
}
126+
}
127+
13128
int main(int argc, char *argv[]) {
14129
if (argc != 2) {
15130
fprintf(stderr, "Usage: schemavalidator schema.json < input.json\n");
@@ -65,6 +180,8 @@ int main(int argc, char *argv[]) {
65180
validator.GetInvalidSchemaPointer().StringifyUriFragment(sb);
66181
fprintf(stderr, "Invalid schema: %s\n", sb.GetString());
67182
fprintf(stderr, "Invalid keyword: %s\n", validator.GetInvalidSchemaKeyword());
183+
fprintf(stderr, "Invalid code: %d\n", validator.GetInvalidSchemaCode());
184+
fprintf(stderr, "Invalid message: %s\n", GetValidateError_En(validator.GetInvalidSchemaCode()));
68185
sb.Clear();
69186
validator.GetInvalidDocumentPointer().StringifyUriFragment(sb);
70187
fprintf(stderr, "Invalid document: %s\n", sb.GetString());
@@ -73,6 +190,7 @@ int main(int argc, char *argv[]) {
73190
PrettyWriter<StringBuffer> w(sb);
74191
validator.GetError().Accept(w);
75192
fprintf(stderr, "Error report:\n%s\n", sb.GetString());
193+
CreateErrorMessages(validator.GetError());
76194
return EXIT_FAILURE;
77195
}
78196
}

include/rapidjson/error/en.h

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,54 @@ inline const RAPIDJSON_ERROR_CHARTYPE* GetParseError_En(ParseErrorCode parseErro
6565
}
6666
}
6767

68+
//! Maps error code of validation into error message.
69+
/*!
70+
\ingroup RAPIDJSON_ERRORS
71+
\param validateErrorCode Error code obtained from validator.
72+
\return the error message.
73+
\note User can make a copy of this function for localization.
74+
Using switch-case is safer for future modification of error codes.
75+
*/
76+
inline const RAPIDJSON_ERROR_CHARTYPE* GetValidateError_En(ValidateErrorCode validateErrorCode) {
77+
switch (validateErrorCode) {
78+
case kValidateErrors: return RAPIDJSON_ERROR_STRING("One or more validation errors have occurred");
79+
case kValidateErrorNone: return RAPIDJSON_ERROR_STRING("No error.");
80+
81+
case kValidateErrorMultipleOf: return RAPIDJSON_ERROR_STRING("Number '%actual' is not a multiple of the 'multipleOf' value '%expected'.");
82+
case kValidateErrorMaximum: return RAPIDJSON_ERROR_STRING("Number '%actual' is greater than the 'maximum' value '%expected'.");
83+
case kValidateErrorExclusiveMaximum: return RAPIDJSON_ERROR_STRING("Number '%actual' is greater than or equal to the 'exclusiveMaximum' value '%expected'.");
84+
case kValidateErrorMinimum: return RAPIDJSON_ERROR_STRING("Number '%actual' is less than the 'minimum' value '%expected'.");
85+
case kValidateErrorExclusiveMinimum: return RAPIDJSON_ERROR_STRING("Number '%actual' is less than or equal to the 'exclusiveMinimum' value '%expected'.");
86+
87+
case kValidateErrorMaxLength: return RAPIDJSON_ERROR_STRING("String '%actual' is longer than the 'maxLength' value '%expected'.");
88+
case kValidateErrorMinLength: return RAPIDJSON_ERROR_STRING("String '%actual' is shorter than the 'minLength' value '%expected'.");
89+
case kValidateErrorPattern: return RAPIDJSON_ERROR_STRING("String '%actual' does not match the 'pattern' regular expression.");
90+
91+
case kValidateErrorMaxItems: return RAPIDJSON_ERROR_STRING("Array of length '%actual' is longer than the 'maxItems' value '%expected'.");
92+
case kValidateErrorMinItems: return RAPIDJSON_ERROR_STRING("Array of length '%actual' is shorter than the 'minItems' value '%expected'.");
93+
case kValidateErrorUniqueItems: return RAPIDJSON_ERROR_STRING("Array has duplicate items at indices '%duplicates' but 'uniqueItems' is true.");
94+
case kValidateErrorAdditionalItems: return RAPIDJSON_ERROR_STRING("Array has an additional item at index '%disallowed' that is not allowed by the schema.");
95+
96+
case kValidateErrorMaxProperties: return RAPIDJSON_ERROR_STRING("Object has '%actual' members which is more than 'maxProperties' value '%expected'.");
97+
case kValidateErrorMinProperties: return RAPIDJSON_ERROR_STRING("Object has '%actual' members which is less than 'minProperties' value '%expected'.");
98+
case kValidateErrorRequired: return RAPIDJSON_ERROR_STRING("Object is missing the following members required by the schema: '%missing'.");
99+
case kValidateErrorAdditionalProperties: return RAPIDJSON_ERROR_STRING("Object has an additional member '%disallowed' that is not allowed by the schema.");
100+
case kValidateErrorPatternProperties: return RAPIDJSON_ERROR_STRING("Object has 'patternProperties' that are not allowed by the schema.");
101+
case kValidateErrorDependencies: return RAPIDJSON_ERROR_STRING("Object has missing property or schema dependencies, refer to following errors.");
102+
103+
case kValidateErrorEnum: return RAPIDJSON_ERROR_STRING("Property has a value that is not one of its allowed enumerated values.");
104+
case kValidateErrorType: return RAPIDJSON_ERROR_STRING("Property has a type '%actual' that is not in the following list: '%expected'.");
105+
106+
case kValidateErrorOneOf: return RAPIDJSON_ERROR_STRING("Property did not match any of the sub-schemas specified by 'oneOf', refer to following errors.");
107+
case kValidateErrorOneOfMatch: return RAPIDJSON_ERROR_STRING("Property matched more than one of the sub-schemas specified by 'oneOf'.");
108+
case kValidateErrorAllOf: return RAPIDJSON_ERROR_STRING("Property did not match all of the sub-schemas specified by 'allOf', refer to following errors.");
109+
case kValidateErrorAnyOf: return RAPIDJSON_ERROR_STRING("Property did not match any of the sub-schemas specified by 'anyOf', refer to following errors.");
110+
case kValidateErrorNot: return RAPIDJSON_ERROR_STRING("Property matched the sub-schema specified by 'not'.");
111+
112+
default: return RAPIDJSON_ERROR_STRING("Unknown error.");
113+
}
114+
}
115+
68116
RAPIDJSON_NAMESPACE_END
69117

70118
#ifdef __clang__

0 commit comments

Comments
 (0)