Merge pull request #57 from cristiangraz/feature/format

Add support for email format as well as the ability to define custom formats
diff --git a/README.md b/README.md
index 7c99649..e5f0797 100644
--- a/README.md
+++ b/README.md
@@ -201,6 +201,34 @@
 ```
 %field% must be greater than or equal to %min%
 ```
+
+## Formats
+JSON Schema allows for optional "format" property to validate strings against well-known formats. gojsonschema ships with all of the formats defined in the spec that you can use like this:
+````json
+{"type": "string", "format": "email"}
+````
+Available formats: date-time, hostname, email, ipv4, ipv4, uri
+
+For repetitive or more complex formats, you can create custom format checkers and add them to gojsonschema like this:
+
+```go
+// Define the format checker
+type RoleFormatChecker struct {}
+
+// Ensure it meets the gojsonschema.FormatChecker interface
+func (f RoleFormatChecker) IsFormat(input string) bool {
+    return strings.HasPrefix("ROLE_", input)
+}
+
+// Add it to the library
+gojsonschema.FormatCheckers.Add("role", RoleFormatChecker{})
+````
+
+Now to use in your json schema:
+````json
+{"type": "string", "format": "role"}
+````
+
 ## Uses
 
 gojsonschema uses the following test suite :
diff --git a/errors.go b/errors.go
index 61c9639..6c82c26 100644
--- a/errors.go
+++ b/errors.go
@@ -106,6 +106,11 @@
 		ResultErrorFields
 	}
 
+	// DoesNotMatchFormatError. ErrorDetails: format
+	DoesNotMatchFormatError struct {
+		ResultErrorFields
+	}
+
 	// MultipleOfError. ErrorDetails: multiple
 	MultipleOfError struct {
 		ResultErrorFields
@@ -197,6 +202,9 @@
 	case *DoesNotMatchPatternError:
 		t = "pattern"
 		d = locale.DoesNotMatchPattern()
+	case *DoesNotMatchFormatError:
+		t = "format"
+		d = locale.DoesNotMatchFormat()
 	case *MultipleOfError:
 		t = "multiple_of"
 		d = locale.MultipleOf()
diff --git a/format_checkers.go b/format_checkers.go
new file mode 100644
index 0000000..a6dff32
--- /dev/null
+++ b/format_checkers.go
@@ -0,0 +1,167 @@
+package gojsonschema
+
+import (
+	"net"
+	"net/url"
+	"reflect"
+	"regexp"
+	"time"
+)
+
+type (
+	// FormatChecker is the interface all formatters added to FormatCheckerChain must implement
+	FormatChecker interface {
+		IsFormat(input string) bool
+	}
+
+	// FormatCheckerChain holds the formatters
+	FormatCheckerChain struct {
+		formatters map[string]FormatChecker
+	}
+
+	// EmailFormatter verifies email address formats
+	EmailFormatChecker struct{}
+
+	// IPV4FormatChecker verifies IP addresses in the ipv4 format
+	IPV4FormatChecker struct{}
+
+	// IPV6FormatChecker verifies IP addresses in the ipv6 format
+	IPV6FormatChecker struct{}
+
+	// DateTimeFormatChecker verifies date/time formats per RFC3339 5.6
+	//
+	// Valid formats:
+	// 		Partial Time: HH:MM:SS
+	//		Full Date: YYYY-MM-DD
+	// 		Full Time: HH:MM:SSZ-07:00
+	//		Date Time: YYYY-MM-DDTHH:MM:SSZ-0700
+	//
+	// 	Where
+	//		YYYY = 4DIGIT year
+	//		MM = 2DIGIT month ; 01-12
+	//		DD = 2DIGIT day-month ; 01-28, 01-29, 01-30, 01-31 based on month/year
+	//		HH = 2DIGIT hour ; 00-23
+	//		MM = 2DIGIT ; 00-59
+	//		SS = 2DIGIT ; 00-58, 00-60 based on leap second rules
+	//		T = Literal
+	//		Z = Literal
+	//
+	//	Note: Nanoseconds are also suported in all formats
+	//
+	// http://tools.ietf.org/html/rfc3339#section-5.6
+	DateTimeFormatChecker struct{}
+
+	// URIFormatCheckers validates a URI with a valid Scheme per RFC3986
+	URIFormatChecker struct{}
+
+	// HostnameFormatChecker validates a hostname is in the correct format
+	HostnameFormatChecker struct{}
+)
+
+var (
+	// Formatters holds the valid formatters, and is a public variable
+	// so library users can add custom formatters
+	FormatCheckers = FormatCheckerChain{
+		formatters: map[string]FormatChecker{
+			"date-time": DateTimeFormatChecker{},
+			"hostname":  HostnameFormatChecker{},
+			"email":     EmailFormatChecker{},
+			"ipv4":      IPV4FormatChecker{},
+			"ipv6":      IPV6FormatChecker{},
+			"uri":       URIFormatChecker{},
+		},
+	}
+
+	// Regex credit: https://github.com/asaskevich/govalidator
+	rxEmail = regexp.MustCompile("^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$")
+
+	// Regex credit: https://www.socketloop.com/tutorials/golang-validate-hostname
+	rxHostname = regexp.MustCompile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`)
+)
+
+// Add adds a FormatChecker to the FormatCheckerChain
+// The name used will be the value used for the format key in your json schema
+func (c *FormatCheckerChain) Add(name string, f FormatChecker) *FormatCheckerChain {
+	c.formatters[name] = f
+
+	return c
+}
+
+// Remove deletes a FormatChecker from the FormatCheckerChain (if it exists)
+func (c *FormatCheckerChain) Remove(name string) *FormatCheckerChain {
+	delete(c.formatters, name)
+
+	return c
+}
+
+// Has checks to see if the FormatCheckerChain holds a FormatChecker with the given name
+func (c *FormatCheckerChain) Has(name string) bool {
+	_, ok := c.formatters[name]
+
+	return ok
+}
+
+// IsFormat will check an input against a FormatChecker with the given name
+// to see if it is the correct format
+func (c *FormatCheckerChain) IsFormat(name string, input interface{}) bool {
+	f, ok := c.formatters[name]
+
+	if !ok {
+		return false
+	}
+
+	if !isKind(input, reflect.String) {
+		return false
+	}
+
+	inputString := input.(string)
+
+	return f.IsFormat(inputString)
+}
+
+func (f EmailFormatChecker) IsFormat(input string) bool {
+	return rxEmail.MatchString(input)
+}
+
+// Credit: https://github.com/asaskevich/govalidator
+func (f IPV4FormatChecker) IsFormat(input string) bool {
+	ip := net.ParseIP(input)
+	return ip != nil && ip.To4() != nil
+}
+
+// Credit: https://github.com/asaskevich/govalidator
+func (f IPV6FormatChecker) IsFormat(input string) bool {
+	ip := net.ParseIP(input)
+	return ip != nil && ip.To4() == nil
+}
+
+func (f DateTimeFormatChecker) IsFormat(input string) bool {
+	formats := []string{
+		"15:04:05",
+		"15:04:05Z07:00",
+		"2006-01-02",
+		time.RFC3339,
+		time.RFC3339Nano,
+	}
+
+	for _, format := range formats {
+		if _, err := time.Parse(format, input); err == nil {
+			return true
+		}
+	}
+
+	return false
+}
+
+func (f URIFormatChecker) IsFormat(input string) bool {
+	u, err := url.Parse(input)
+	if err != nil || u.Scheme == "" {
+		return false
+	}
+
+	return true
+}
+
+func (f HostnameFormatChecker) IsFormat(input string) bool {
+	return rxHostname.MatchString(input)
+}
diff --git a/json_schema_test_suite/format/data_00.json b/json_schema_test_suite/format/data_00.json
new file mode 100644
index 0000000..60bc259
--- /dev/null
+++ b/json_schema_test_suite/format/data_00.json
@@ -0,0 +1 @@
+"test"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_01.json b/json_schema_test_suite/format/data_01.json
new file mode 100644
index 0000000..6d9ec40
--- /dev/null
+++ b/json_schema_test_suite/format/data_01.json
@@ -0,0 +1 @@
+"test@"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_02.json b/json_schema_test_suite/format/data_02.json
new file mode 100644
index 0000000..6b8674d
--- /dev/null
+++ b/json_schema_test_suite/format/data_02.json
@@ -0,0 +1 @@
+"test@test.com"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_03.json b/json_schema_test_suite/format/data_03.json
new file mode 100644
index 0000000..970043c
--- /dev/null
+++ b/json_schema_test_suite/format/data_03.json
@@ -0,0 +1 @@
+"AB-10105"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_04.json b/json_schema_test_suite/format/data_04.json
new file mode 100644
index 0000000..140843f
--- /dev/null
+++ b/json_schema_test_suite/format/data_04.json
@@ -0,0 +1 @@
+"ABC10105"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_05.json b/json_schema_test_suite/format/data_05.json
new file mode 100644
index 0000000..e87a8ac
--- /dev/null
+++ b/json_schema_test_suite/format/data_05.json
@@ -0,0 +1 @@
+"05:15:37"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_06.json b/json_schema_test_suite/format/data_06.json
new file mode 100644
index 0000000..17f91eb
--- /dev/null
+++ b/json_schema_test_suite/format/data_06.json
@@ -0,0 +1 @@
+"2015-05-13"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_07.json b/json_schema_test_suite/format/data_07.json
new file mode 100644
index 0000000..f0218e9
--- /dev/null
+++ b/json_schema_test_suite/format/data_07.json
@@ -0,0 +1 @@
+"2015-6-31"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_08.json b/json_schema_test_suite/format/data_08.json
new file mode 100644
index 0000000..bf9e258
--- /dev/null
+++ b/json_schema_test_suite/format/data_08.json
@@ -0,0 +1 @@
+"2015-01-30 19:08:06"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_09.json b/json_schema_test_suite/format/data_09.json
new file mode 100644
index 0000000..1a57beb
--- /dev/null
+++ b/json_schema_test_suite/format/data_09.json
@@ -0,0 +1 @@
+"18:31:24-05:00"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_10.json b/json_schema_test_suite/format/data_10.json
new file mode 100644
index 0000000..00ed4ee
--- /dev/null
+++ b/json_schema_test_suite/format/data_10.json
@@ -0,0 +1 @@
+"2002-10-02T10:00:00-05:00"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_11.json b/json_schema_test_suite/format/data_11.json
new file mode 100644
index 0000000..a8f92d5
--- /dev/null
+++ b/json_schema_test_suite/format/data_11.json
@@ -0,0 +1 @@
+"2002-10-02T15:00:00Z"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_12.json b/json_schema_test_suite/format/data_12.json
new file mode 100644
index 0000000..0975607
--- /dev/null
+++ b/json_schema_test_suite/format/data_12.json
@@ -0,0 +1 @@
+"2002-10-02T15:00:00.05Z"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_13.json b/json_schema_test_suite/format/data_13.json
new file mode 100644
index 0000000..9f9dace
--- /dev/null
+++ b/json_schema_test_suite/format/data_13.json
@@ -0,0 +1 @@
+"example.com"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_14.json b/json_schema_test_suite/format/data_14.json
new file mode 100644
index 0000000..15a9a87
--- /dev/null
+++ b/json_schema_test_suite/format/data_14.json
@@ -0,0 +1 @@
+"sub.example.com"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_15.json b/json_schema_test_suite/format/data_15.json
new file mode 100644
index 0000000..5c8e9b7
--- /dev/null
+++ b/json_schema_test_suite/format/data_15.json
@@ -0,0 +1 @@
+"hello.co.uk"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_16.json b/json_schema_test_suite/format/data_16.json
new file mode 100644
index 0000000..f5a11ec
--- /dev/null
+++ b/json_schema_test_suite/format/data_16.json
@@ -0,0 +1 @@
+"http://example.com"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_17.json b/json_schema_test_suite/format/data_17.json
new file mode 100644
index 0000000..062bf8c
--- /dev/null
+++ b/json_schema_test_suite/format/data_17.json
@@ -0,0 +1 @@
+"example_com"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_18.json b/json_schema_test_suite/format/data_18.json
new file mode 100644
index 0000000..fd44410
--- /dev/null
+++ b/json_schema_test_suite/format/data_18.json
@@ -0,0 +1 @@
+"4.2.2.4"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_19.json b/json_schema_test_suite/format/data_19.json
new file mode 100644
index 0000000..1e490c6
--- /dev/null
+++ b/json_schema_test_suite/format/data_19.json
@@ -0,0 +1 @@
+"4.1.1111.45"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_20.json b/json_schema_test_suite/format/data_20.json
new file mode 100644
index 0000000..2891810
--- /dev/null
+++ b/json_schema_test_suite/format/data_20.json
@@ -0,0 +1 @@
+"FE80:0000:0000:0000:0202:B3FF:FE1E:8329"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_21.json b/json_schema_test_suite/format/data_21.json
new file mode 100644
index 0000000..ad20ef3
--- /dev/null
+++ b/json_schema_test_suite/format/data_21.json
@@ -0,0 +1 @@
+"FE80::0202:B3FF:FE1E:8329"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_22.json b/json_schema_test_suite/format/data_22.json
new file mode 100644
index 0000000..97dae8f
--- /dev/null
+++ b/json_schema_test_suite/format/data_22.json
@@ -0,0 +1 @@
+"1200::AB00:1234::2552:7777:1313"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_23.json b/json_schema_test_suite/format/data_23.json
new file mode 100644
index 0000000..ef38c10
--- /dev/null
+++ b/json_schema_test_suite/format/data_23.json
@@ -0,0 +1 @@
+"1200:0000:AB00:1234:O000:2552:7777:1313"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_24.json b/json_schema_test_suite/format/data_24.json
new file mode 100644
index 0000000..4187b90
--- /dev/null
+++ b/json_schema_test_suite/format/data_24.json
@@ -0,0 +1 @@
+"ftp://ftp.is.co.za/rfc/rfc1808.txt"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_25.json b/json_schema_test_suite/format/data_25.json
new file mode 100644
index 0000000..7816838
--- /dev/null
+++ b/json_schema_test_suite/format/data_25.json
@@ -0,0 +1 @@
+"mailto:john.doe@example.com"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_26.json b/json_schema_test_suite/format/data_26.json
new file mode 100644
index 0000000..df1cfae
--- /dev/null
+++ b/json_schema_test_suite/format/data_26.json
@@ -0,0 +1 @@
+"tel:+1-816-555-1212"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_27.json b/json_schema_test_suite/format/data_27.json
new file mode 100644
index 0000000..28995dd
--- /dev/null
+++ b/json_schema_test_suite/format/data_27.json
@@ -0,0 +1 @@
+"http://www.ietf.org/rfc/rfc2396.txt"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/data_28.json b/json_schema_test_suite/format/data_28.json
new file mode 100644
index 0000000..6b620ed
--- /dev/null
+++ b/json_schema_test_suite/format/data_28.json
@@ -0,0 +1 @@
+"example.com/path/to/file"
\ No newline at end of file
diff --git a/json_schema_test_suite/format/schema_0.json b/json_schema_test_suite/format/schema_0.json
new file mode 100644
index 0000000..b54a202
--- /dev/null
+++ b/json_schema_test_suite/format/schema_0.json
@@ -0,0 +1 @@
+{"type": "string", "format": "email"}
\ No newline at end of file
diff --git a/json_schema_test_suite/format/schema_1.json b/json_schema_test_suite/format/schema_1.json
new file mode 100644
index 0000000..7df6711
--- /dev/null
+++ b/json_schema_test_suite/format/schema_1.json
@@ -0,0 +1 @@
+{"type": "string", "format": "invoice"}
\ No newline at end of file
diff --git a/json_schema_test_suite/format/schema_2.json b/json_schema_test_suite/format/schema_2.json
new file mode 100644
index 0000000..0dc4b8f
--- /dev/null
+++ b/json_schema_test_suite/format/schema_2.json
@@ -0,0 +1 @@
+{"type": "string", "format": "date-time"}
\ No newline at end of file
diff --git a/json_schema_test_suite/format/schema_3.json b/json_schema_test_suite/format/schema_3.json
new file mode 100644
index 0000000..d1cf1a0
--- /dev/null
+++ b/json_schema_test_suite/format/schema_3.json
@@ -0,0 +1 @@
+{"type": "string", "format": "hostname"}
\ No newline at end of file
diff --git a/json_schema_test_suite/format/schema_4.json b/json_schema_test_suite/format/schema_4.json
new file mode 100644
index 0000000..8468509
--- /dev/null
+++ b/json_schema_test_suite/format/schema_4.json
@@ -0,0 +1 @@
+{"type": "string", "format": "ipv4"}
\ No newline at end of file
diff --git a/json_schema_test_suite/format/schema_5.json b/json_schema_test_suite/format/schema_5.json
new file mode 100644
index 0000000..ddee847
--- /dev/null
+++ b/json_schema_test_suite/format/schema_5.json
@@ -0,0 +1 @@
+{"type": "string", "format": "ipv6"}
\ No newline at end of file
diff --git a/json_schema_test_suite/format/schema_6.json b/json_schema_test_suite/format/schema_6.json
new file mode 100644
index 0000000..493546d
--- /dev/null
+++ b/json_schema_test_suite/format/schema_6.json
@@ -0,0 +1 @@
+{"type": "string", "format": "uri"}
\ No newline at end of file
diff --git a/locales.go b/locales.go
index 2312a23..de05d60 100644
--- a/locales.go
+++ b/locales.go
@@ -48,6 +48,7 @@
 		StringGTE() string
 		StringLTE() string
 		DoesNotMatchPattern() string
+		DoesNotMatchFormat() string
 		MultipleOf() string
 		NumberGTE() string
 		NumberGT() string
@@ -63,6 +64,7 @@
 		CannotBeGT() string
 		MustBeOfType() string
 		MustBeValidRegex() string
+		MustBeValidFormat() string
 		MustBeGTEZero() string
 		KeyCannotBeGreaterThan() string
 		KeyItemsMustBeOfType() string
@@ -160,6 +162,10 @@
 	return `Does not match pattern '%pattern%'`
 }
 
+func (l DefaultLocale) DoesNotMatchFormat() string {
+	return `Does not match format '%format%'`
+}
+
 func (l DefaultLocale) MultipleOf() string {
 	return `Must be a multiple of %multiple%`
 }
@@ -213,6 +219,10 @@
 	return `%key% must be a valid regex`
 }
 
+func (l DefaultLocale) MustBeValidFormat() string {
+	return `%key% must be a valid format %given%`
+}
+
 func (l DefaultLocale) MustBeGTEZero() string {
 	return `%key% must be greater than or equal to 0`
 }
diff --git a/schema.go b/schema.go
index f00674e..577c786 100644
--- a/schema.go
+++ b/schema.go
@@ -534,6 +534,18 @@
 		}
 	}
 
+	if existsMapKey(m, KEY_FORMAT) {
+		formatString, ok := m[KEY_FORMAT].(string)
+		if ok && FormatCheckers.Has(formatString) {
+			currentSchema.format = formatString
+		} else {
+			return errors.New(formatErrorDescription(
+				Locale.MustBeValidFormat(),
+				ErrorDetails{"key": KEY_FORMAT, "given": m[KEY_FORMAT]},
+			))
+		}
+	}
+
 	// validation : object
 
 	if existsMapKey(m, KEY_MIN_PROPERTIES) {
diff --git a/schema_test.go b/schema_test.go
index 01ff133..db8d00d 100644
--- a/schema_test.go
+++ b/schema_test.go
@@ -29,12 +29,27 @@
 	"fmt"
 	"net/http"
 	"os"
+	"regexp"
 	"strconv"
 	"testing"
 )
 
 const displayErrorMessages = false
 
+var rxInvoice = regexp.MustCompile("^[A-Z]{2}-[0-9]{5}")
+
+// Used for remote schema in ref/schema_5.json that defines "uri" and "regex" types
+type alwaysTrueFormatChecker struct{}
+type invoiceFormatChecker struct{}
+
+func (a alwaysTrueFormatChecker) IsFormat(input string) bool {
+	return true
+}
+
+func (a invoiceFormatChecker) IsFormat(input string) bool {
+	return rxInvoice.MatchString(input)
+}
+
 func TestJsonSchemaTestSuite(t *testing.T) {
 
 	JsonSchemaTestSuiteMap := []map[string]string{
@@ -286,7 +301,38 @@
 		map[string]string{"phase": "fragment within remote ref", "test": "remote fragment valid", "schema": "refRemote/schema_1.json", "data": "refRemote/data_10.json", "valid": "true"},
 		map[string]string{"phase": "fragment within remote ref", "test": "remote fragment invalid", "schema": "refRemote/schema_1.json", "data": "refRemote/data_11.json", "valid": "false"},
 		map[string]string{"phase": "ref within remote ref", "test": "ref within ref valid", "schema": "refRemote/schema_2.json", "data": "refRemote/data_20.json", "valid": "true"},
-		map[string]string{"phase": "ref within remote ref", "test": "ref within ref invalid", "schema": "refRemote/schema_2.json", "data": "refRemote/data_21.json", "valid": "false"}}
+		map[string]string{"phase": "ref within remote ref", "test": "ref within ref invalid", "schema": "refRemote/schema_2.json", "data": "refRemote/data_21.json", "valid": "false"},
+		map[string]string{"phase": "format validation", "test": "email format is invalid", "schema": "format/schema_0.json", "data": "format/data_00.json", "valid": "false"},
+		map[string]string{"phase": "format validation", "test": "email format is invalid", "schema": "format/schema_0.json", "data": "format/data_01.json", "valid": "false"},
+		map[string]string{"phase": "format validation", "test": "email format valid", "schema": "format/schema_0.json", "data": "format/data_02.json", "valid": "true"},
+		map[string]string{"phase": "format validation", "test": "invoice format valid", "schema": "format/schema_1.json", "data": "format/data_03.json", "valid": "true"},
+		map[string]string{"phase": "format validation", "test": "invoice format is invalid", "schema": "format/schema_1.json", "data": "format/data_04.json", "valid": "false"},
+		map[string]string{"phase": "format validation", "test": "date-time format is valid", "schema": "format/schema_2.json", "data": "format/data_05.json", "valid": "true"},
+		map[string]string{"phase": "format validation", "test": "date-time format is valid", "schema": "format/schema_2.json", "data": "format/data_06.json", "valid": "true"},
+		map[string]string{"phase": "format validation", "test": "date-time format is invalid", "schema": "format/schema_2.json", "data": "format/data_07.json", "valid": "false"},
+		map[string]string{"phase": "format validation", "test": "date-time format is valid", "schema": "format/schema_2.json", "data": "format/data_08.json", "valid": "false"},
+		map[string]string{"phase": "format validation", "test": "date-time format is valid", "schema": "format/schema_2.json", "data": "format/data_09.json", "valid": "true"},
+		map[string]string{"phase": "format validation", "test": "date-time format is valid", "schema": "format/schema_2.json", "data": "format/data_10.json", "valid": "true"},
+		map[string]string{"phase": "format validation", "test": "date-time format is valid", "schema": "format/schema_2.json", "data": "format/data_11.json", "valid": "true"},
+		map[string]string{"phase": "format validation", "test": "date-time format is valid", "schema": "format/schema_2.json", "data": "format/data_12.json", "valid": "true"},
+		map[string]string{"phase": "format validation", "test": "hostname format is valid", "schema": "format/schema_3.json", "data": "format/data_13.json", "valid": "true"},
+		map[string]string{"phase": "format validation", "test": "hostname format is valid", "schema": "format/schema_3.json", "data": "format/data_14.json", "valid": "true"},
+		map[string]string{"phase": "format validation", "test": "hostname format is valid", "schema": "format/schema_3.json", "data": "format/data_15.json", "valid": "true"},
+		map[string]string{"phase": "format validation", "test": "hostname format is invalid", "schema": "format/schema_3.json", "data": "format/data_16.json", "valid": "false"},
+		map[string]string{"phase": "format validation", "test": "hostname format is invalid", "schema": "format/schema_3.json", "data": "format/data_17.json", "valid": "false"},
+		map[string]string{"phase": "format validation", "test": "ipv4 format is valid", "schema": "format/schema_4.json", "data": "format/data_18.json", "valid": "true"},
+		map[string]string{"phase": "format validation", "test": "ipv4 format is invalid", "schema": "format/schema_4.json", "data": "format/data_19.json", "valid": "false"},
+		map[string]string{"phase": "format validation", "test": "ipv6 format is valid", "schema": "format/schema_5.json", "data": "format/data_20.json", "valid": "true"},
+		map[string]string{"phase": "format validation", "test": "ipv6 format is valid", "schema": "format/schema_5.json", "data": "format/data_21.json", "valid": "true"},
+		map[string]string{"phase": "format validation", "test": "ipv6 format is invalid", "schema": "format/schema_5.json", "data": "format/data_22.json", "valid": "false"},
+		map[string]string{"phase": "format validation", "test": "ipv6 format is invalid", "schema": "format/schema_5.json", "data": "format/data_23.json", "valid": "false"},
+		map[string]string{"phase": "format validation", "test": "uri format is valid", "schema": "format/schema_6.json", "data": "format/data_24.json", "valid": "true"},
+		map[string]string{"phase": "format validation", "test": "uri format is valid", "schema": "format/schema_6.json", "data": "format/data_25.json", "valid": "true"},
+		map[string]string{"phase": "format validation", "test": "uri format is valid", "schema": "format/schema_6.json", "data": "format/data_26.json", "valid": "true"},
+		map[string]string{"phase": "format validation", "test": "uri format is valid", "schema": "format/schema_6.json", "data": "format/data_27.json", "valid": "true"},
+		map[string]string{"phase": "format validation", "test": "uri format is invalid", "schema": "format/schema_6.json", "data": "format/data_28.json", "valid": "false"},
+		map[string]string{"phase": "format validation", "test": "uri format is invalid", "schema": "format/schema_6.json", "data": "format/data_13.json", "valid": "false"},
+	}
 
 	//TODO Pass failed tests : id(s) as scope for references is not implemented yet
 	//map[string]string{"phase": "change resolution scope", "test": "changed scope ref valid", "schema": "refRemote/schema_3.json", "data": "refRemote/data_30.json", "valid": "true"},
@@ -308,6 +354,12 @@
 		}
 	}()
 
+	// Used for remote schema in ref/schema_5.json that defines "regex" type
+	FormatCheckers.Add("regex", alwaysTrueFormatChecker{})
+
+	// Custom Formatter
+	FormatCheckers.Add("invoice", invoiceFormatChecker{})
+
 	// Launch tests
 
 	for testJsonIndex, testJson := range JsonSchemaTestSuiteMap {
diff --git a/subSchema.go b/subSchema.go
index fa8124d..b249b7e 100644
--- a/subSchema.go
+++ b/subSchema.go
@@ -55,6 +55,7 @@
 	KEY_MIN_LENGTH            = "minLength"
 	KEY_MAX_LENGTH            = "maxLength"
 	KEY_PATTERN               = "pattern"
+	KEY_FORMAT                = "format"
 	KEY_MIN_PROPERTIES        = "minProperties"
 	KEY_MAX_PROPERTIES        = "maxProperties"
 	KEY_DEPENDENCIES          = "dependencies"
@@ -107,6 +108,7 @@
 	minLength *int
 	maxLength *int
 	pattern   *regexp.Regexp
+	format    string
 
 	// validation : object
 	minProperties *int
diff --git a/validation.go b/validation.go
index c0d7d05..0531dc7 100644
--- a/validation.go
+++ b/validation.go
@@ -698,6 +698,18 @@
 		}
 	}
 
+	// format
+	if currentSubSchema.format != "" {
+		if !FormatCheckers.IsFormat(currentSubSchema.format, stringValue) {
+			result.addError(
+				new(DoesNotMatchFormatError),
+				context,
+				value,
+				ErrorDetails{"format": currentSubSchema.format},
+			)
+		}
+	}
+
 	result.incrementScore()
 }