Repository: jsonrainbow/json-schema (language: php)

Issue:
Format attribute should not validate data instances that aren't the instance type to validate
Our bowtie report shows that for [Draft 3](https://bowtie.report/#/dialects/draft3?language=php) and [Draft 4](https://bowtie.report/#/dialects/draft4?language=php) has some failing tests with regards to the format attribute.

Some example of these failing test case are:

- Test: all string formats ignore integers
  - Case: email format
  - Case: ipv4 format
  - Case: ipv6 format

# Draft 3 specification
From the spec we can read the following
>[7.1.](https://json-schema.org/draft-04/draft-fge-json-schema-validation-00#rfc.section.7.1) Foreword
Structural validation alone may be insufficient to validate that an instance meets all the requirements of an application. The "format" keyword is defined to allow interoperable semantic validation for a fixed subset of values which are accurately described by authoritative resources, be they RFCs or other external specifications.
>
>The value of this keyword is called a format attribute. It MUST be a string. A format attribute can generally only validate a given set of instance types. **If the type of the instance to validate is not in this set, validation for this format attribute and instance SHOULD succeed.**


Discussion / hints:
@DannyvdSluijs I will give this a try.

Should this be fixed in `5.x` as well or is it sufficient to fix it in `master` and port it to `6.0.x`?
@W0rma that would be great.

Based on the biggest problem for this repo (which is time from contributors, including myself) I would focus the fix on master.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b2736caa..5b519bc9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fix wrong combined paths when traversing upward, fixes #557 ([#652](https://github.com/jsonrainbow/json-schema/pull/652))
 - Correct PHPStan baseline ([#764](https://github.com/jsonrainbow/json-schema/pull/764))
 - Correct spacing issue in `README.md` ([#763](https://github.com/jsonrainbow/json-schema/pull/763))
+- Format attribute: do not validate data instances that aren't the instance type to validate ([#773](https://github.com/jsonrainbow/json-schema/pull/773))
 
 ### Changed
 - Bump to minimum PHP 7.2 ([#746](https://github.com/jsonrainbow/json-schema/pull/746))
diff --git a/src/JsonSchema/Constraints/FormatConstraint.php b/src/JsonSchema/Constraints/FormatConstraint.php
index dbc35475..afb27b0f 100644
--- a/src/JsonSchema/Constraints/FormatConstraint.php
+++ b/src/JsonSchema/Constraints/FormatConstraint.php
@@ -35,7 +35,7 @@ public function check(&$element, $schema = null, ?JsonPointer $path = null, $i =
 
         switch ($schema->format) {
             case 'date':
-                if (!$date = $this->validateDateTime($element, 'Y-m-d')) {
+                if (is_string($element) && !$date = $this->validateDateTime($element, 'Y-m-d')) {
                     $this->addError(ConstraintError::FORMAT_DATE(), $path, [
                             'date' => $element,
                             'format' => $schema->format
@@ -45,7 +45,7 @@ public function check(&$element, $schema = null, ?JsonPointer $path = null, $i =
                 break;
 
             case 'time':
-                if (!$this->validateDateTime($element, 'H:i:s')) {
+                if (is_string($element) && !$this->validateDateTime($element, 'H:i:s')) {
                     $this->addError(ConstraintError::FORMAT_TIME(), $path, [
                             'time' => json_encode($element),
                             'format' => $schema->format,
@@ -55,7 +55,7 @@ public function check(&$element, $schema = null, ?JsonPointer $path = null, $i =
                 break;
 
             case 'date-time':
-                if (null === Rfc3339::createFromString($element)) {
+                if (is_string($element) && null === Rfc3339::createFromString($element)) {
                     $this->addError(ConstraintError::FORMAT_DATE_TIME(), $path, [
                             'dateTime' => json_encode($element),
                             'format' => $schema->format
@@ -101,14 +101,14 @@ public function check(&$element, $schema = null, ?JsonPointer $path = null, $i =
                 break;
 
             case 'uri':
-                if (null === filter_var($element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE)) {
+                if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE)) {
                     $this->addError(ConstraintError::FORMAT_URL(), $path, ['format' => $schema->format]);
                 }
                 break;
 
             case 'uriref':
             case 'uri-reference':
-                if (null === filter_var($element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE)) {
+                if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE)) {
                     // FILTER_VALIDATE_URL does not conform to RFC-3986, and cannot handle relative URLs, but
                     // the json-schema spec uses RFC-3986, so need a bit of hackery to properly validate them.
                     // See https://tools.ietf.org/html/rfc3986#section-4.2 for additional information.
@@ -133,20 +133,20 @@ public function check(&$element, $schema = null, ?JsonPointer $path = null, $i =
                 break;
 
             case 'email':
-                if (null === filter_var($element, FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE | FILTER_FLAG_EMAIL_UNICODE)) {
+                if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE | FILTER_FLAG_EMAIL_UNICODE)) {
                     $this->addError(ConstraintError::FORMAT_EMAIL(), $path, ['format' => $schema->format]);
                 }
                 break;
 
             case 'ip-address':
             case 'ipv4':
-                if (null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4)) {
+                if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4)) {
                     $this->addError(ConstraintError::FORMAT_IP(), $path, ['format' => $schema->format]);
                 }
                 break;
 
             case 'ipv6':
-                if (null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV6)) {
+                if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV6)) {
                     $this->addError(ConstraintError::FORMAT_IP(), $path, ['format' => $schema->format]);
                 }
                 break;
@@ -186,11 +186,19 @@ protected function validateDateTime($datetime, $format)
 
     protected function validateRegex($regex)
     {
+        if (!is_string($regex)) {
+            return true;
+        }
+
         return false !== @preg_match(self::jsonPatternToPhpRegex($regex), '');
     }
 
     protected function validateColor($color)
     {
+        if (!is_string($color)) {
+            return true;
+        }
+
         if (in_array(strtolower($color), ['aqua', 'black', 'blue', 'fuchsia',
             'gray', 'green', 'lime', 'maroon', 'navy', 'olive', 'orange', 'purple',
             'red', 'silver', 'teal', 'white', 'yellow'])) {
@@ -215,6 +223,10 @@ protected function validatePhone($phone)
 
     protected function validateHostname($host)
     {
+        if (!is_string($host)) {
+            return true;
+        }
+
         $hostnameRegex = '/^(([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])$/i';
 
         return preg_match($hostnameRegex, $host);


Test patch:
diff --git a/tests/Constraints/FormatTest.php b/tests/Constraints/FormatTest.php
index 2d91e5fe..25eab1c0 100644
--- a/tests/Constraints/FormatTest.php
+++ b/tests/Constraints/FormatTest.php
@@ -100,10 +100,14 @@ public function getValidFormats(): array
         return [
             ['2001-01-23', 'date'],
             ['2000-02-29', 'date'],
+            [42, 'date'],
+            [4.2, 'date'],
 
             ['12:22:01', 'time'],
             ['00:00:00', 'time'],
             ['23:59:59', 'time'],
+            [42, 'time'],
+            [4.2, 'time'],
 
             ['2000-05-01T12:12:12Z', 'date-time'],
             ['2000-05-01T12:12:12+0100', 'date-time'],
@@ -114,6 +118,8 @@ public function getValidFormats(): array
             ['2000-05-01T12:12:12.0Z', 'date-time'],
             ['2000-05-01T12:12:12.000Z', 'date-time'],
             ['2000-05-01T12:12:12.000000Z', 'date-time'],
+            [42, 'date-time'],
+            [4.2, 'date-time'],
 
             ['0', 'utc-millisec'],
 
@@ -136,6 +142,8 @@ public function getValidFormats(): array
             ['yellow', 'color'],
             ['#fff', 'color'],
             ['#00cc00', 'color'],
+            [42, 'color'],
+            [4.2, 'color'],
 
             ['background: blue', 'style'],
             ['color: #000;', 'style'],
@@ -149,18 +157,39 @@ public function getValidFormats(): array
             ['./relative:PathReference/', 'uri-reference'],
             ['relativePathReference/', 'uri-reference'],
             ['relative/Path:Reference/', 'uri-reference'],
+            [42, 'uri-reference'],
+            [4.2, 'uri-reference'],
 
             ['info@something.edu', 'email'],
+            [42, 'email'],
+            [4.2, 'email'],
 
             ['10.10.10.10', 'ip-address'],
             ['127.0.0.1', 'ip-address'],
+            [42, 'ip-address'],
+            [4.2, 'ip-address'],
+
+            ['127.0.0.1', 'ipv4'],
+            [42, 'ipv4'],
+            [4.2, 'ipv4'],
 
             ['::ff', 'ipv6'],
+            [42, 'ipv6'],
+            [4.2, 'ipv6'],
 
             ['www.example.com', 'host-name'],
             ['3v4l.org', 'host-name'],
             ['a-valid-host.com', 'host-name'],
             ['localhost', 'host-name'],
+            [42, 'host-name'],
+            [4.2, 'host-name'],
+
+            ['www.example.com', 'hostname'],
+            ['3v4l.org', 'hostname'],
+            ['a-valid-host.com', 'hostname'],
+            ['localhost', 'hostname'],
+            [42, 'hostname'],
+            [4.2, 'hostname'],
 
             ['anything', '*'],
             ['unknown', '*'],