Merge branch 'hoshsadiq-master'
diff --git a/.gitignore b/.gitignore
index c1e0636..4c60981 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
*.sw[nop]
+*.iml
diff --git a/README.md b/README.md
index fe43659..e822195 100644
--- a/README.md
+++ b/README.md
@@ -190,12 +190,14 @@
**err.Value()**: *interface{}* Returns the value given
-**err.Context()**: *gojsonschema.jsonContext* Returns the context. This has a String() method that will print something like this: (root).firstName
+**err.Context()**: *gojsonschema.JsonContext* Returns the context. This has a String() method that will print something like this: (root).firstName
**err.Field()**: *string* Returns the fieldname in the format firstName, or for embedded properties, person.firstName. This returns the same as the String() method on *err.Context()* but removes the (root). prefix.
**err.Description()**: *string* The error description. This is based on the locale you are using. See the beginning of this section for overwriting the locale with a custom implementation.
+**err.DescriptionFormat()**: *string* The error description format. This is relevant if you are adding custom validation errors afterwards to the result.
+
**err.Details()**: *gojsonschema.ErrorDetails* Returns a map[string]interface{} of additional error details specific to the error. For example, GTE errors will have a "min" value, LTE will have a "max" value. See errors.go for a full description of all the error details. Every error always contains a "field" key that holds the value of *err.Field()*
Note in most cases, the err.Details() will be used to generate replacement strings in your locales, and not used directly. These strings follow the text/template format i.e.
@@ -286,7 +288,56 @@
gojsonschema.FormatCheckers.Add("ValidUserId", ValidUserIdFormatChecker{})
````
+## Additional custom validation
+After the validation has run and you have the results, you may add additional
+errors using `Result.AddError`. This is useful to maintain the same format within the resultset instead
+of having to add special exceptions for your own errors. Below is an example.
+```go
+type AnswerInvalidError struct {
+ gojsonschema.ResultErrorFields
+}
+
+func newAnswerInvalidError(context *gojsonschema.JsonContext, value interface{}, details gojsonschema.ErrorDetails) *AnswerInvalidError {
+ err := AnswerInvalidError{}
+ err.SetContext(context)
+ err.SetType("custom_invalid_error")
+ // it is important to use SetDescriptionFormat() as this is used to call SetDescription() after it has been parsed
+ // using the description of err will be overridden by this.
+ err.SetDescriptionFormat("Answer to the Ultimate Question of Life, the Universe, and Everything is {{.answer}}")
+ err.SetValue(value)
+ err.SetDetails(details)
+
+ return &err
+}
+
+func main() {
+ // ...
+ schema, err := gojsonschema.NewSchema(schemaLoader)
+ result, err := gojsonschema.Validate(schemaLoader, documentLoader)
+
+ if true { // some validation
+ jsonContext := gojsonschema.NewJsonContext("question", nil)
+ errDetail := gojsonschema.ErrorDetails{
+ "answer": 42,
+ }
+ result.AddError(
+ newAnswerInvalidError(
+ gojsonschema.NewJsonContext("answer", jsonContext),
+ 52,
+ errDetail,
+ ),
+ errDetail,
+ )
+ }
+
+ return result, err
+
+}
+```
+
+This is especially useful if you want to add validation beyond what the
+json schema drafts can provide such business specific logic.
## Uses
diff --git a/errors.go b/errors.go
index 58cb01f..e583986 100644
--- a/errors.go
+++ b/errors.go
@@ -158,7 +158,7 @@
)
// newError takes a ResultError type and sets the type, context, description, details, value, and field
-func newError(err ResultError, context *jsonContext, value interface{}, locale locale, details ErrorDetails) {
+func newError(err ResultError, context *JsonContext, value interface{}, locale locale, details ErrorDetails) {
var t string
var d string
switch err.(type) {
@@ -252,13 +252,14 @@
err.SetContext(context)
err.SetValue(value)
err.SetDetails(details)
+ err.SetDescriptionFormat(d)
details["field"] = err.Field()
if _, exists := details["context"]; !exists && context != nil {
details["context"] = context.String()
}
- err.SetDescription(formatErrorDescription(d, details))
+ err.SetDescription(formatErrorDescription(err.DescriptionFormat(), details))
}
// formatErrorDescription takes a string in the default text/template
diff --git a/jsonContext.go b/jsonContext.go
index fcc8d9d..f40668a 100644
--- a/jsonContext.go
+++ b/jsonContext.go
@@ -26,20 +26,20 @@
import "bytes"
-// jsonContext implements a persistent linked-list of strings
-type jsonContext struct {
+// JsonContext implements a persistent linked-list of strings
+type JsonContext struct {
head string
- tail *jsonContext
+ tail *JsonContext
}
-func newJsonContext(head string, tail *jsonContext) *jsonContext {
- return &jsonContext{head, tail}
+func NewJsonContext(head string, tail *JsonContext) *JsonContext {
+ return &JsonContext{head, tail}
}
// String displays the context in reverse.
// This plays well with the data structure's persistent nature with
// Cons and a json document's tree structure.
-func (c *jsonContext) String(del ...string) string {
+func (c *JsonContext) String(del ...string) string {
byteArr := make([]byte, 0, c.stringLen())
buf := bytes.NewBuffer(byteArr)
c.writeStringToBuffer(buf, del)
@@ -47,7 +47,7 @@
return buf.String()
}
-func (c *jsonContext) stringLen() int {
+func (c *JsonContext) stringLen() int {
length := 0
if c.tail != nil {
length = c.tail.stringLen() + 1 // add 1 for "."
@@ -57,7 +57,7 @@
return length
}
-func (c *jsonContext) writeStringToBuffer(buf *bytes.Buffer, del []string) {
+func (c *JsonContext) writeStringToBuffer(buf *bytes.Buffer, del []string) {
if c.tail != nil {
c.tail.writeStringToBuffer(buf, del)
diff --git a/result.go b/result.go
index 6ad56ae..2e1e8d6 100644
--- a/result.go
+++ b/result.go
@@ -40,10 +40,12 @@
Field() string
SetType(string)
Type() string
- SetContext(*jsonContext)
- Context() *jsonContext
+ SetContext(*JsonContext)
+ Context() *JsonContext
SetDescription(string)
Description() string
+ SetDescriptionFormat(string)
+ DescriptionFormat() string
SetValue(interface{})
Value() interface{}
SetDetails(ErrorDetails)
@@ -55,11 +57,12 @@
// ResultErrorFields implements the ResultError interface, so custom errors
// can be defined by just embedding this type
ResultErrorFields struct {
- errorType string // A string with the type of error (i.e. invalid_type)
- context *jsonContext // Tree like notation of the part that failed the validation. ex (root).a.b ...
- description string // A human readable error message
- value interface{} // Value given by the JSON file that is the source of the error
- details ErrorDetails
+ errorType string // A string with the type of error (i.e. invalid_type)
+ context *JsonContext // Tree like notation of the part that failed the validation. ex (root).a.b ...
+ description string // A human readable error message
+ descriptionFormat string // A format for human readable error message
+ value interface{} // Value given by the JSON file that is the source of the error
+ details ErrorDetails
}
Result struct {
@@ -90,11 +93,11 @@
return v.errorType
}
-func (v *ResultErrorFields) SetContext(context *jsonContext) {
+func (v *ResultErrorFields) SetContext(context *JsonContext) {
v.context = context
}
-func (v *ResultErrorFields) Context() *jsonContext {
+func (v *ResultErrorFields) Context() *JsonContext {
return v.context
}
@@ -106,6 +109,14 @@
return v.description
}
+func (v *ResultErrorFields) SetDescriptionFormat(descriptionFormat string) {
+ v.descriptionFormat = descriptionFormat
+}
+
+func (v *ResultErrorFields) DescriptionFormat() string {
+ return v.descriptionFormat
+}
+
func (v *ResultErrorFields) SetValue(value interface{}) {
v.value = value
}
@@ -154,8 +165,19 @@
func (v *Result) Errors() []ResultError {
return v.errors
}
+// Add a fully filled error to the error set
+// SetDescription() will be called with the result of the parsed err.DescriptionFormat()
+func (v *Result) AddError(err ResultError, details ErrorDetails) {
+ if _, exists := details["context"]; !exists && err.Context() != nil {
+ details["context"] = err.Context().String()
+ }
-func (v *Result) addError(err ResultError, context *jsonContext, value interface{}, details ErrorDetails) {
+ err.SetDescription(formatErrorDescription(err.DescriptionFormat(), details))
+
+ v.errors = append(v.errors, err)
+}
+
+func (v *Result) addInternalError(err ResultError, context *JsonContext, value interface{}, details ErrorDetails) {
newError(err, context, value, Locale, details)
v.errors = append(v.errors, err)
v.score -= 2 // results in a net -1 when added to the +1 we get at the end of the validation function
diff --git a/validation.go b/validation.go
index 664da21..54fc7a7 100644
--- a/validation.go
+++ b/validation.go
@@ -64,21 +64,21 @@
// begin validation
result := &Result{}
- context := newJsonContext(STRING_CONTEXT_ROOT, nil)
+ context := NewJsonContext(STRING_CONTEXT_ROOT, nil)
v.rootSchema.validateRecursive(v.rootSchema, root, result, context)
return result, nil
}
-func (v *subSchema) subValidateWithContext(document interface{}, context *jsonContext) *Result {
+func (v *subSchema) subValidateWithContext(document interface{}, context *JsonContext) *Result {
result := &Result{}
v.validateRecursive(v, document, result, context)
return result
}
// Walker function to validate the json recursively against the subSchema
-func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode interface{}, result *Result, context *jsonContext) {
+func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode interface{}, result *Result, context *JsonContext) {
if internalLogEnabled {
internalLog("validateRecursive %s", context.String())
@@ -94,7 +94,7 @@
// Check for null value
if currentNode == nil {
if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_NULL) {
- result.addError(
+ result.addInternalError(
new(InvalidTypeError),
context,
currentNode,
@@ -126,7 +126,7 @@
givenType = TYPE_NUMBER
}
- result.addError(
+ result.addInternalError(
new(InvalidTypeError),
context,
currentNode,
@@ -155,7 +155,7 @@
case reflect.Slice:
if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_ARRAY) {
- result.addError(
+ result.addInternalError(
new(InvalidTypeError),
context,
currentNode,
@@ -178,7 +178,7 @@
case reflect.Map:
if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_OBJECT) {
- result.addError(
+ result.addInternalError(
new(InvalidTypeError),
context,
currentNode,
@@ -203,7 +203,7 @@
for _, pSchema := range currentSubSchema.propertiesChildren {
nextNode, ok := castCurrentNode[pSchema.property]
if ok {
- subContext := newJsonContext(pSchema.property, context)
+ subContext := NewJsonContext(pSchema.property, context)
v.validateRecursive(pSchema, nextNode, result, subContext)
}
}
@@ -213,7 +213,7 @@
case reflect.Bool:
if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_BOOLEAN) {
- result.addError(
+ result.addInternalError(
new(InvalidTypeError),
context,
currentNode,
@@ -235,7 +235,7 @@
case reflect.String:
if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_STRING) {
- result.addError(
+ result.addInternalError(
new(InvalidTypeError),
context,
currentNode,
@@ -264,7 +264,7 @@
}
// Different kinds of validation there, subSchema / common / array / object / string...
-func (v *subSchema) validateSchema(currentSubSchema *subSchema, currentNode interface{}, result *Result, context *jsonContext) {
+func (v *subSchema) validateSchema(currentSubSchema *subSchema, currentNode interface{}, result *Result, context *JsonContext) {
if internalLogEnabled {
internalLog("validateSchema %s", context.String())
@@ -288,7 +288,7 @@
}
if !validatedAnyOf {
- result.addError(new(NumberAnyOfError), context, currentNode, ErrorDetails{})
+ result.addInternalError(new(NumberAnyOfError), context, currentNode, ErrorDetails{})
if bestValidationResult != nil {
// add error messages of closest matching subSchema as
@@ -314,7 +314,7 @@
if nbValidated != 1 {
- result.addError(new(NumberOneOfError), context, currentNode, ErrorDetails{})
+ result.addInternalError(new(NumberOneOfError), context, currentNode, ErrorDetails{})
if nbValidated == 0 {
// add error messages of closest matching subSchema as
@@ -337,14 +337,14 @@
}
if nbValidated != len(currentSubSchema.allOf) {
- result.addError(new(NumberAllOfError), context, currentNode, ErrorDetails{})
+ result.addInternalError(new(NumberAllOfError), context, currentNode, ErrorDetails{})
}
}
if currentSubSchema.not != nil {
validationResult := currentSubSchema.not.subValidateWithContext(currentNode, context)
if validationResult.Valid() {
- result.addError(new(NumberNotError), context, currentNode, ErrorDetails{})
+ result.addInternalError(new(NumberNotError), context, currentNode, ErrorDetails{})
}
}
@@ -357,7 +357,7 @@
case []string:
for _, dependOnKey := range dependency {
if _, dependencyResolved := currentNode.(map[string]interface{})[dependOnKey]; !dependencyResolved {
- result.addError(
+ result.addInternalError(
new(MissingDependencyError),
context,
currentNode,
@@ -380,14 +380,14 @@
if currentSubSchema._then != nil && validationResultIf.Valid() {
validationResultThen := currentSubSchema._then.subValidateWithContext(currentNode, context)
if !validationResultThen.Valid() {
- result.addError(new(ConditionThenError), context, currentNode, ErrorDetails{})
+ result.addInternalError(new(ConditionThenError), context, currentNode, ErrorDetails{})
result.mergeErrors(validationResultThen)
}
}
if currentSubSchema._else != nil && !validationResultIf.Valid() {
validationResultElse := currentSubSchema._else.subValidateWithContext(currentNode, context)
if !validationResultElse.Valid() {
- result.addError(new(ConditionElseError), context, currentNode, ErrorDetails{})
+ result.addInternalError(new(ConditionElseError), context, currentNode, ErrorDetails{})
result.mergeErrors(validationResultElse)
}
}
@@ -396,7 +396,7 @@
result.incrementScore()
}
-func (v *subSchema) validateCommon(currentSubSchema *subSchema, value interface{}, result *Result, context *jsonContext) {
+func (v *subSchema) validateCommon(currentSubSchema *subSchema, value interface{}, result *Result, context *JsonContext) {
if internalLogEnabled {
internalLog("validateCommon %s", context.String())
@@ -407,10 +407,10 @@
if len(currentSubSchema.enum) > 0 {
has, err := currentSubSchema.ContainsEnum(value)
if err != nil {
- result.addError(new(InternalError), context, value, ErrorDetails{"error": err})
+ result.addInternalError(new(InternalError), context, value, ErrorDetails{"error": err})
}
if !has {
- result.addError(
+ result.addInternalError(
new(EnumError),
context,
value,
@@ -424,7 +424,7 @@
result.incrementScore()
}
-func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface{}, result *Result, context *jsonContext) {
+func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface{}, result *Result, context *JsonContext) {
if internalLogEnabled {
internalLog("validateArray %s", context.String())
@@ -436,7 +436,7 @@
// TODO explain
if currentSubSchema.itemsChildrenIsSingleSchema {
for i := range value {
- subContext := newJsonContext(strconv.Itoa(i), context)
+ subContext := NewJsonContext(strconv.Itoa(i), context)
validationResult := currentSubSchema.itemsChildren[0].subValidateWithContext(value[i], subContext)
result.mergeErrors(validationResult)
}
@@ -447,7 +447,7 @@
// while we have both schemas and values, check them against each other
for i := 0; i != nbItems && i != nbValues; i++ {
- subContext := newJsonContext(strconv.Itoa(i), context)
+ subContext := NewJsonContext(strconv.Itoa(i), context)
validationResult := currentSubSchema.itemsChildren[i].subValidateWithContext(value[i], subContext)
result.mergeErrors(validationResult)
}
@@ -459,12 +459,12 @@
switch currentSubSchema.additionalItems.(type) {
case bool:
if !currentSubSchema.additionalItems.(bool) {
- result.addError(new(ArrayNoAdditionalItemsError), context, value, ErrorDetails{})
+ result.addInternalError(new(ArrayNoAdditionalItemsError), context, value, ErrorDetails{})
}
case *subSchema:
additionalItemSchema := currentSubSchema.additionalItems.(*subSchema)
for i := nbItems; i != nbValues; i++ {
- subContext := newJsonContext(strconv.Itoa(i), context)
+ subContext := NewJsonContext(strconv.Itoa(i), context)
validationResult := additionalItemSchema.subValidateWithContext(value[i], subContext)
result.mergeErrors(validationResult)
}
@@ -476,7 +476,7 @@
// minItems & maxItems
if currentSubSchema.minItems != nil {
if nbValues < int(*currentSubSchema.minItems) {
- result.addError(
+ result.addInternalError(
new(ArrayMinItemsError),
context,
value,
@@ -486,7 +486,7 @@
}
if currentSubSchema.maxItems != nil {
if nbValues > int(*currentSubSchema.maxItems) {
- result.addError(
+ result.addInternalError(
new(ArrayMaxItemsError),
context,
value,
@@ -501,10 +501,10 @@
for _, v := range value {
vString, err := marshalToJsonString(v)
if err != nil {
- result.addError(new(InternalError), context, value, ErrorDetails{"err": err})
+ result.addInternalError(new(InternalError), context, value, ErrorDetails{"err": err})
}
if isStringInSlice(stringifiedItems, *vString) {
- result.addError(
+ result.addInternalError(
new(ItemsMustBeUniqueError),
context,
value,
@@ -518,7 +518,7 @@
result.incrementScore()
}
-func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string]interface{}, result *Result, context *jsonContext) {
+func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string]interface{}, result *Result, context *JsonContext) {
if internalLogEnabled {
internalLog("validateObject %s", context.String())
@@ -528,7 +528,7 @@
// minProperties & maxProperties:
if currentSubSchema.minProperties != nil {
if len(value) < int(*currentSubSchema.minProperties) {
- result.addError(
+ result.addInternalError(
new(ArrayMinPropertiesError),
context,
value,
@@ -538,7 +538,7 @@
}
if currentSubSchema.maxProperties != nil {
if len(value) > int(*currentSubSchema.maxProperties) {
- result.addError(
+ result.addInternalError(
new(ArrayMaxPropertiesError),
context,
value,
@@ -553,7 +553,7 @@
if ok {
result.incrementScore()
} else {
- result.addError(
+ result.addInternalError(
new(RequiredError),
context,
value,
@@ -584,7 +584,7 @@
if found {
if pp_has && !pp_match {
- result.addError(
+ result.addInternalError(
new(AdditionalPropertyNotAllowedError),
context,
value[pk],
@@ -595,7 +595,7 @@
} else {
if !pp_has || !pp_match {
- result.addError(
+ result.addInternalError(
new(AdditionalPropertyNotAllowedError),
context,
value[pk],
@@ -647,7 +647,7 @@
if pp_has && !pp_match {
- result.addError(
+ result.addInternalError(
new(InvalidPropertyPatternError),
context,
value[pk],
@@ -664,7 +664,7 @@
result.incrementScore()
}
-func (v *subSchema) validatePatternProperty(currentSubSchema *subSchema, key string, value interface{}, result *Result, context *jsonContext) (has bool, matched bool) {
+func (v *subSchema) validatePatternProperty(currentSubSchema *subSchema, key string, value interface{}, result *Result, context *JsonContext) (has bool, matched bool) {
if internalLogEnabled {
internalLog("validatePatternProperty %s", context.String())
@@ -678,7 +678,7 @@
for pk, pv := range currentSubSchema.patternProperties {
if matches, _ := regexp.MatchString(pk, key); matches {
has = true
- subContext := newJsonContext(key, context)
+ subContext := NewJsonContext(key, context)
validationResult := pv.subValidateWithContext(value, subContext)
result.mergeErrors(validationResult)
if validationResult.Valid() {
@@ -696,7 +696,7 @@
return has, true
}
-func (v *subSchema) validateString(currentSubSchema *subSchema, value interface{}, result *Result, context *jsonContext) {
+func (v *subSchema) validateString(currentSubSchema *subSchema, value interface{}, result *Result, context *JsonContext) {
// Ignore JSON numbers
if isJsonNumber(value) {
@@ -718,7 +718,7 @@
// minLength & maxLength:
if currentSubSchema.minLength != nil {
if utf8.RuneCount([]byte(stringValue)) < int(*currentSubSchema.minLength) {
- result.addError(
+ result.addInternalError(
new(StringLengthGTEError),
context,
value,
@@ -728,7 +728,7 @@
}
if currentSubSchema.maxLength != nil {
if utf8.RuneCount([]byte(stringValue)) > int(*currentSubSchema.maxLength) {
- result.addError(
+ result.addInternalError(
new(StringLengthLTEError),
context,
value,
@@ -740,7 +740,7 @@
// pattern:
if currentSubSchema.pattern != nil {
if !currentSubSchema.pattern.MatchString(stringValue) {
- result.addError(
+ result.addInternalError(
new(DoesNotMatchPatternError),
context,
value,
@@ -753,7 +753,7 @@
// format
if currentSubSchema.format != "" {
if !FormatCheckers.IsFormat(currentSubSchema.format, stringValue) {
- result.addError(
+ result.addInternalError(
new(DoesNotMatchFormatError),
context,
value,
@@ -765,7 +765,7 @@
result.incrementScore()
}
-func (v *subSchema) validateNumber(currentSubSchema *subSchema, value interface{}, result *Result, context *jsonContext) {
+func (v *subSchema) validateNumber(currentSubSchema *subSchema, value interface{}, result *Result, context *JsonContext) {
// Ignore non numbers
if !isJsonNumber(value) {
@@ -784,7 +784,7 @@
if currentSubSchema.multipleOf != nil {
if q := new(big.Float).Quo(float64Value, currentSubSchema.multipleOf); !q.IsInt() {
- result.addError(
+ result.addInternalError(
new(MultipleOfError),
context,
resultErrorFormatJsonNumber(number),
@@ -797,7 +797,7 @@
if currentSubSchema.maximum != nil {
if currentSubSchema.exclusiveMaximum {
if float64Value.Cmp(currentSubSchema.maximum) >= 0 {
- result.addError(
+ result.addInternalError(
new(NumberLTError),
context,
resultErrorFormatJsonNumber(number),
@@ -808,7 +808,7 @@
}
} else {
if float64Value.Cmp(currentSubSchema.maximum) == 1 {
- result.addError(
+ result.addInternalError(
new(NumberLTEError),
context,
resultErrorFormatJsonNumber(number),
@@ -825,7 +825,7 @@
if currentSubSchema.exclusiveMinimum {
if float64Value.Cmp(currentSubSchema.minimum) <= 0 {
// if float64Value <= *currentSubSchema.minimum {
- result.addError(
+ result.addInternalError(
new(NumberGTError),
context,
resultErrorFormatJsonNumber(number),
@@ -836,7 +836,7 @@
}
} else {
if float64Value.Cmp(currentSubSchema.minimum) == -1 {
- result.addError(
+ result.addInternalError(
new(NumberGTEError),
context,
resultErrorFormatJsonNumber(number),
@@ -851,7 +851,7 @@
// format
if currentSubSchema.format != "" {
if !FormatCheckers.IsFormat(currentSubSchema.format, float64Value) {
- result.addError(
+ result.addInternalError(
new(DoesNotMatchFormatError),
context,
value,