Repository: go-playground/validator
Pull request #571 (2020-02-04 16:09:41)

Add isJSON validation
- [x] Tests exist or have been written that cover this particular change.
- [x] doc.go was updated
- [x] en translation and translation tests were updated

Change Details:

This PR add a new validation that checks if a string value is valid JSON. The implementation uses Go's json.Valid method.

@go-playground/admins


Patch:
diff --git a/baked_in.go b/baked_in.go
index 13ec143..deb00a2 100644
--- a/baked_in.go
+++ b/baked_in.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"context"
 	"crypto/sha256"
+	"encoding/json"
 	"fmt"
 	"net"
 	"net/url"
@@ -166,6 +167,7 @@ var (
 		"html_encoded":         isHTMLEncoded,
 		"url_encoded":          isURLEncoded,
 		"dir":                  isDir,
+		"json":                 isJSON,
 		"hostname_port":        isHostnamePort,
 		"lowercase":            isLowercase,
 		"uppercase":            isUppercase,
@@ -2011,6 +2013,17 @@ func isDir(fl FieldLevel) bool {
 	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
 }
 
+// isJSON is the validation function for validating if the current field's value is a valid json string.
+func isJSON(fl FieldLevel) bool {
+	field := fl.Field()
+
+	if field.Kind() == reflect.String {
+		val := field.String()
+		return json.Valid([]byte(val))
+  }
+  panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+}
+
 // isHostnamePort validates a <dns>:<port> combination for fields typically used for socket address.
 func isHostnamePort(fl FieldLevel) bool {
 	val := fl.Field().String()
diff --git a/doc.go b/doc.go
index 3f304f6..9bcba9a 100644
--- a/doc.go
+++ b/doc.go
@@ -685,6 +685,12 @@ does any email provider accept all possibilities.
 
 	Usage: email
 
+JSON String
+
+This validates that a string value is valid JSON
+
+	Usage: json
+
 File path
 
 This validates that a string value contains a valid file path and that
diff --git a/translations/en/en.go b/translations/en/en.go
index cf89fd2..be61d85 100644
--- a/translations/en/en.go
+++ b/translations/en/en.go
@@ -1322,6 +1322,11 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er
 			},
 		},
 		{
+			tag:         "json",
+			translation: "{0} must be a valid json string",
+      override:    false,
+    },
+    {
 			tag:         "lowercase",
 			translation: "{0} must be a lowercase string",
 			override:    false,


Test patch:
diff --git a/translations/en/en_test.go b/translations/en/en_test.go
index 769409a..b95ca98 100644
--- a/translations/en/en_test.go
+++ b/translations/en/en_test.go
@@ -141,6 +141,7 @@ func TestTranslations(t *testing.T) {
 		UniqueSlice       []string          `validate:"unique"`
 		UniqueArray       [3]string         `validate:"unique"`
 		UniqueMap         map[string]string `validate:"unique"`
+		JSONString        string            `validate:"json"`
 		LowercaseString   string            `validate:"lowercase"`
 		UppercaseString   string            `validate:"uppercase"`
 	}
@@ -637,6 +638,10 @@ func TestTranslations(t *testing.T) {
 			ns:       "Test.UniqueMap",
 			expected: "UniqueMap must contain unique values",
 		},
+		{
+			ns:       "Test.JSONString",
+			expected: "JSONString must be a valid json string",
+		},
 		{
 			ns:       "Test.LowercaseString",
 			expected: "LowercaseString must be a lowercase string",
diff --git a/validator_test.go b/validator_test.go
index 7c831ed..1323867 100644
--- a/validator_test.go
+++ b/validator_test.go
@@ -9004,6 +9004,54 @@ func TestGetTag(t *testing.T) {
 	Equal(t, tag, "mytag")
 }
 
+func TestJSONValidation(t *testing.T) {
+	tests := []struct {
+		param    string
+		expected bool
+	}{
+		{`foo`, false},
+		{`}{`, false},
+		{`{]`, false},
+		{`{}`, true},
+		{`{"foo":"bar"}`, true},
+		{`{"foo":"bar","bar":{"baz":["qux"]}}`, true},
+		{`{"foo": 3 "bar": 4}`, false},
+		{`{"foo": 3 ,"bar": 4`, false},
+		{`{foo": 3, "bar": 4}`, false},
+		{`foo`, false},
+		{`1`, true},
+		{`true`, true},
+		{`null`, true},
+		{`"null"`, true},
+	}
+
+	validate := New()
+
+	for i, test := range tests {
+
+		errs := validate.Var(test.param, "json")
+
+		if test.expected {
+			if !IsEqual(errs, nil) {
+				t.Fatalf("Index: %d json failed Error: %s", i, errs)
+			}
+		} else {
+			if IsEqual(errs, nil) {
+				t.Fatalf("Index: %d json failed Error: %s", i, errs)
+			} else {
+				val := getError(errs, "", "")
+				if val.Tag() != "json" {
+					t.Fatalf("Index: %d json failed Error: %s", i, errs)
+				}
+			}
+		}
+	}
+
+	PanicMatches(t, func() {
+		_ = validate.Var(2, "json")
+	}, "Bad field type int")
+}
+
 func Test_hostnameport_validator(t *testing.T) {
 
 	type Host struct {
@@ -9107,4 +9155,4 @@ func TestUppercaseValidation(t *testing.T) {
 	PanicMatches(t, func() {
 		_ = validate.Var(2, "uppercase")
 	}, "Bad field type int")
-}
+}
\ No newline at end of file