Repository: networknt/json-schema-validator
Pull request #149 (2019-05-21 18:42:07)

Added DateTimeValidator
Related issue: https://github.com/networknt/json-schema-validator/issues/138


Patch:
diff --git a/src/main/java/com/networknt/schema/DateTimeValidator.java b/src/main/java/com/networknt/schema/DateTimeValidator.java
new file mode 100644
index 0000000..179de39
--- /dev/null
+++ b/src/main/java/com/networknt/schema/DateTimeValidator.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2016 Network New Technologies Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.networknt.schema;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class DateTimeValidator extends BaseJsonValidator implements JsonValidator {
+    private static final Logger logger = LoggerFactory.getLogger(DateTimeValidator.class);
+
+    private final String formatName;
+    private final String DATE = "date";
+    private final String DATETIME = "date-time";
+
+    private static final Pattern RFC3339_PATTERN = Pattern.compile(
+            "^(\\d{4})-(\\d{2})-(\\d{2})" // yyyy-MM-dd
+                    + "([Tt](\\d{2}):(\\d{2}):(\\d{2})(\\.\\d+)?)?" // 'T'HH:mm:ss.milliseconds
+                    + "([Zz]|([+-])(\\d{2}):(\\d{2}))?");
+
+    public DateTimeValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, String formatName) {
+        super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.DATETIME, validationContext);
+        this.formatName = formatName;
+        parseErrorCode(getValidatorType().getErrorCodeKey());
+    }
+
+    public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
+        debug(logger, node, rootNode, at);
+
+        Set<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
+
+        JsonType nodeType = TypeFactory.getValueNodeType(node);
+        if (nodeType != JsonType.STRING) {
+            return errors;
+        }
+        if (!isLegalDateTime(node.textValue())) {
+            errors.add(buildValidationMessage(at, node.textValue(), formatName));
+        }
+        return Collections.unmodifiableSet(errors);
+    }
+
+    private boolean isLegalDateTime(String string) {
+        Matcher matcher = RFC3339_PATTERN.matcher(string);
+        StringBuilder pattern = new StringBuilder();
+        StringBuilder dateTime = new StringBuilder();
+        // Validate the format
+        if (!matcher.matches()) {
+            logger.error("Failed to apply RFC3339 pattern on " + string);
+            return false;
+        }
+        // Validate the date/time content
+        String year = matcher.group(1);
+        String month = matcher.group(2);
+        String day = matcher.group(3);
+        dateTime.append(year).append('-').append(month).append('-').append(day);
+        pattern.append("yyyy-MM-dd");
+
+        boolean isTimeGiven = matcher.group(4) != null;
+        String timeZoneShiftRegexGroup = matcher.group(9);
+        boolean isTimeZoneShiftGiven = timeZoneShiftRegexGroup != null;
+        String hour = null;
+        String minute = null;
+        String second = null;
+        String milliseconds = null;
+        String timeShiftHour = null;
+        String timeShiftMinute = null;
+
+        if (!isTimeGiven && DATETIME.equals(formatName) || (isTimeGiven && DATE.equals(formatName))) {
+            logger.error("The supplied date/time format type does not match the specification, expected: " + formatName);
+            return false;
+        }
+
+        if (!isTimeGiven && isTimeZoneShiftGiven) {
+            logger.error("Invalid date/time format, cannot specify time zone shift" +
+                    " without specifying time: " + string);
+            return false;
+        }
+
+        if (isTimeGiven) {
+            hour = matcher.group(5);
+            minute = matcher.group(6);
+            second = matcher.group(7);
+            dateTime.append('T').append(hour).append(':').append(minute).append(':').append(second);
+            pattern.append("'T'hh:mm:ss");
+            if (matcher.group(8) != null) {
+                // Normalize milliseconds to 3-length digit
+                milliseconds = matcher.group(8);
+                if (milliseconds.length() > 4) {
+                    milliseconds = milliseconds.substring(0, 4);
+                } else {
+                    while (milliseconds.length() < 4) {
+                        milliseconds += "0";
+                    }
+                }
+                dateTime.append(milliseconds);
+                pattern.append(".SSS");
+            }
+        }
+
+        if (isTimeGiven && isTimeZoneShiftGiven) {
+            if (Character.toUpperCase(timeZoneShiftRegexGroup.charAt(0)) == 'Z') {
+                dateTime.append('Z');
+                pattern.append("'Z'");
+            } else {
+                timeShiftHour = matcher.group(11);
+                timeShiftMinute = matcher.group(12);
+                dateTime.append(matcher.group(10).charAt(0)).append(timeShiftHour).append(':').append(timeShiftMinute);
+                pattern.append("XXX");
+            }
+        }
+        return validateDateTime(dateTime.toString(), pattern.toString());
+    }
+
+    private boolean validateDateTime(String dateTime, String pattern) {
+        SimpleDateFormat sdf = new SimpleDateFormat(pattern);
+        sdf.setLenient(false);
+        return sdf.parse(dateTime, new ParsePosition(0)) != null;
+    }
+}
diff --git a/src/main/java/com/networknt/schema/FormatKeyword.java b/src/main/java/com/networknt/schema/FormatKeyword.java
index ec2c019..9344bc1 100644
--- a/src/main/java/com/networknt/schema/FormatKeyword.java
+++ b/src/main/java/com/networknt/schema/FormatKeyword.java
@@ -26,6 +26,9 @@ public class FormatKeyword implements Keyword {
     private final ValidatorTypeCode type;
     private final Map<String, Format> formats;
 
+    private final String DATE = "date";
+    private final String DATE_TIME = "date-time";
+
     public FormatKeyword(ValidatorTypeCode type, Map<String, Format> formats) {
         this.type = type;
         this.formats = formats;
@@ -34,7 +37,7 @@ public class FormatKeyword implements Keyword {
     Collection<Format> getFormats() {
         return Collections.unmodifiableCollection(formats.values());
     }
-    
+
     @Override
     public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext)
             throws Exception {
@@ -42,15 +45,16 @@ public class FormatKeyword implements Keyword {
         if (schemaNode != null && schemaNode.isTextual()) {
             String formatName = schemaNode.textValue();
             format = formats.get(formatName);
+            // Validate date and time separately
+            if (formatName.equals(DATE) || formatName.equals(DATE_TIME)) {
+                return new DateTimeValidator(schemaPath, schemaNode, parentSchema, validationContext, formatName);
+            }
         }
-
         return new FormatValidator(schemaPath, schemaNode, parentSchema, validationContext, format);
     }
-    
+
     @Override
     public String getValue() {
         return type.getValue();
     }
-    
-    
 }
diff --git a/src/main/java/com/networknt/schema/ValidatorTypeCode.java b/src/main/java/com/networknt/schema/ValidatorTypeCode.java
index efda2a5..2dbc02e 100644
--- a/src/main/java/com/networknt/schema/ValidatorTypeCode.java
+++ b/src/main/java/com/networknt/schema/ValidatorTypeCode.java
@@ -1,145 +1,146 @@
-/*
- * Copyright (c) 2016 Network New Technologies Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.networknt.schema;
-
-import java.lang.reflect.Constructor;
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import com.fasterxml.jackson.databind.JsonNode;
-
-public enum ValidatorTypeCode implements Keyword, ErrorMessageType {
-    ADDITIONAL_PROPERTIES("additionalProperties", "1001", new MessageFormat(
-            "{0}.{1}: is not defined in the schema and the schema does not allow additional properties")),
-    ALL_OF("allOf", "1002", new MessageFormat("{0}: should be valid to all the schemas {1}")),
-    ANY_OF("anyOf", "1003", new MessageFormat("{0}: should be valid to any of the schemas {1}")),
-    CROSS_EDITS("crossEdits", "1004", new MessageFormat("{0}: has an error with 'cross edits'")),
-    DEPENDENCIES("dependencies", "1007", new MessageFormat("{0}: has an error with dependencies {1}")),
-    EDITS("edits", "1005", new MessageFormat("{0}: has an error with 'edits'")),
-    ENUM("enum", "1008", new MessageFormat("{0}: does not have a value in the enumeration {1}")),
-    FORMAT("format", "1009", new MessageFormat("{0}: does not match the {1} pattern {2}")){
-        @Override
-        public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext)
-                throws Exception {
-            throw new UnsupportedOperationException("Use FormatKeyword instead");
-        }
-    },
-    ITEMS("items", "1010", new MessageFormat("{0}[{1}]: no validator found at this index")),
-    MAXIMUM("maximum", "1011", new MessageFormat("{0}: must have a maximum value of {1}")),
-    MAX_ITEMS("maxItems", "1012", new MessageFormat("{0}: there must be a maximum of {1} items in the array")),
-    MAX_LENGTH("maxLength", "1013", new MessageFormat("{0}: may only be {1} characters long")),
-    MAX_PROPERTIES("maxProperties", "1014", new MessageFormat("{0}: may only have a maximum of {1} properties")),
-    MINIMUM("minimum", "1015", new MessageFormat("{0}: must have a minimum value of {1}")),
-    MIN_ITEMS("minItems", "1016", new MessageFormat("{0}: there must be a minimum of {1} items in the array")),
-    MIN_LENGTH("minLength", "1017", new MessageFormat("{0}: must be at least {1} characters long")),
-    MIN_PROPERTIES("minProperties", "1018", new MessageFormat("{0}: should have a minimum of {1} properties")),
-    MULTIPLE_OF("multipleOf", "1019", new MessageFormat("{0}: must be multiple of {1}")),
-    NOT_ALLOWED("notAllowed", "1033", new MessageFormat("{0}.{1}: is not allowed but it is in the data")),
-    NOT("not", "1020", new MessageFormat("{0}: should not be valid to the schema {1}")),
-    ONE_OF("oneOf", "1022", new MessageFormat("{0}: should be valid to one and only one of the schemas {1}")),
-    PATTERN_PROPERTIES("patternProperties", "1024", new MessageFormat("{0}: has some error with 'pattern properties'")),
-    PATTERN("pattern", "1023", new MessageFormat("{0}: does not match the regex pattern {1}")),
-    PROPERTIES("properties", "1025", new MessageFormat("{0}: has an error with 'properties'")),
-    READ_ONLY("readOnly", "1032", new MessageFormat("{0}: is a readonly field, it cannot be changed")),
-    REF("$ref", "1026", new MessageFormat("{0}: has an error with 'refs'")),
-    REQUIRED("required", "1028", new MessageFormat("{0}.{1}: is missing but it is required")),
-    TYPE("type", "1029", new MessageFormat("{0}: {1} found, {2} expected")),
-    UNION_TYPE("unionType", "1030", new MessageFormat("{0}: {1} found, but {2} is required")),
-    UNIQUE_ITEMS("uniqueItems", "1031", new MessageFormat("{0}: the items in the array must be unique"));
-   
-	private static Map<String, ValidatorTypeCode> constants = new HashMap<String, ValidatorTypeCode>();
-
-    static {
-        for (ValidatorTypeCode c : values()) {
-            constants.put(c.value, c);
-        }
-    }
-
-    private final String value;
-    private final String errorCode;
-    private final MessageFormat messageFormat;
-    private final String errorCodeKey;
-
-    private ValidatorTypeCode(String value, String errorCode, MessageFormat messageFormat) {
-        this.value = value;
-        this.errorCode = errorCode;
-        this.messageFormat = messageFormat;
-        this.errorCodeKey = value + "ErrorCode";
-    }
-
-    public static List<ValidatorTypeCode> getNonFormatKeywords() {
-        final List<ValidatorTypeCode> result = new ArrayList<ValidatorTypeCode>();
-        for (ValidatorTypeCode keyword: values()) {
-            if (!FORMAT.equals(keyword)) {
-                result.add(keyword);
-            }
-        }
-        return result;
-    }
-
-    public static ValidatorTypeCode fromValue(String value) {
-        ValidatorTypeCode constant = constants.get(value);
-        if (constant == null) {
-            throw new IllegalArgumentException(value);
-        } else {
-            return constant;
-        }
-    }
-    
-    public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws Exception {
-            String shortClassName = getValue();
-            if (shortClassName.startsWith("$")) {
-                // remove "$" from class name for $ref schema
-                shortClassName = shortClassName.substring(1);
-            }
-
-            final String className = Character.toUpperCase(shortClassName.charAt(0)) + shortClassName.substring(1)
-                    + "Validator";
-            @SuppressWarnings("unchecked")
-            final Class<JsonValidator> clazz = (Class<JsonValidator>) Class
-                    .forName("com.networknt.schema." + className);
-            Constructor<JsonValidator> c = null;
-            c = clazz.getConstructor(
-                    new Class[] { String.class, JsonNode.class, JsonSchema.class, ValidationContext.class });
-            return c.newInstance(schemaPath + "/" + getValue(), schemaNode, parentSchema, validationContext);
-    }
-    
-    @Override
-    public String toString() {
-        return this.value;
-    }
-
-    public String getValue() {
-        return value;
-    }
-
-    public String getErrorCode() {
-        return errorCode;
-    }
-
-    public MessageFormat getMessageFormat() {
-        return messageFormat;
-    }
-
-    public String getErrorCodeKey() {
-        return errorCodeKey;
-    }
-
-}
+/*
+ * Copyright (c) 2016 Network New Technologies Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.networknt.schema;
+
+import java.lang.reflect.Constructor;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public enum ValidatorTypeCode implements Keyword, ErrorMessageType {
+    ADDITIONAL_PROPERTIES("additionalProperties", "1001", new MessageFormat(
+            "{0}.{1}: is not defined in the schema and the schema does not allow additional properties")),
+    ALL_OF("allOf", "1002", new MessageFormat("{0}: should be valid to all the schemas {1}")),
+    ANY_OF("anyOf", "1003", new MessageFormat("{0}: should be valid to any of the schemas {1}")),
+    CROSS_EDITS("crossEdits", "1004", new MessageFormat("{0}: has an error with 'cross edits'")),
+    DEPENDENCIES("dependencies", "1007", new MessageFormat("{0}: has an error with dependencies {1}")),
+    EDITS("edits", "1005", new MessageFormat("{0}: has an error with 'edits'")),
+    ENUM("enum", "1008", new MessageFormat("{0}: does not have a value in the enumeration {1}")),
+    FORMAT("format", "1009", new MessageFormat("{0}: does not match the {1} pattern {2}")){
+        @Override
+        public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext)
+                throws Exception {
+            throw new UnsupportedOperationException("Use FormatKeyword instead");
+        }
+    },
+    ITEMS("items", "1010", new MessageFormat("{0}[{1}]: no validator found at this index")),
+    MAXIMUM("maximum", "1011", new MessageFormat("{0}: must have a maximum value of {1}")),
+    MAX_ITEMS("maxItems", "1012", new MessageFormat("{0}: there must be a maximum of {1} items in the array")),
+    MAX_LENGTH("maxLength", "1013", new MessageFormat("{0}: may only be {1} characters long")),
+    MAX_PROPERTIES("maxProperties", "1014", new MessageFormat("{0}: may only have a maximum of {1} properties")),
+    MINIMUM("minimum", "1015", new MessageFormat("{0}: must have a minimum value of {1}")),
+    MIN_ITEMS("minItems", "1016", new MessageFormat("{0}: there must be a minimum of {1} items in the array")),
+    MIN_LENGTH("minLength", "1017", new MessageFormat("{0}: must be at least {1} characters long")),
+    MIN_PROPERTIES("minProperties", "1018", new MessageFormat("{0}: should have a minimum of {1} properties")),
+    MULTIPLE_OF("multipleOf", "1019", new MessageFormat("{0}: must be multiple of {1}")),
+    NOT_ALLOWED("notAllowed", "1033", new MessageFormat("{0}.{1}: is not allowed but it is in the data")),
+    NOT("not", "1020", new MessageFormat("{0}: should not be valid to the schema {1}")),
+    ONE_OF("oneOf", "1022", new MessageFormat("{0}: should be valid to one and only one of the schemas {1}")),
+    PATTERN_PROPERTIES("patternProperties", "1024", new MessageFormat("{0}: has some error with 'pattern properties'")),
+    PATTERN("pattern", "1023", new MessageFormat("{0}: does not match the regex pattern {1}")),
+    PROPERTIES("properties", "1025", new MessageFormat("{0}: has an error with 'properties'")),
+    READ_ONLY("readOnly", "1032", new MessageFormat("{0}: is a readonly field, it cannot be changed")),
+    REF("$ref", "1026", new MessageFormat("{0}: has an error with 'refs'")),
+    REQUIRED("required", "1028", new MessageFormat("{0}.{1}: is missing but it is required")),
+    TYPE("type", "1029", new MessageFormat("{0}: {1} found, {2} expected")),
+    UNION_TYPE("unionType", "1030", new MessageFormat("{0}: {1} found, but {2} is required")),
+    UNIQUE_ITEMS("uniqueItems", "1031", new MessageFormat("{0}: the items in the array must be unique")),
+    DATETIME("date-time", "1034", new MessageFormat("{0}: {1} is an invalid {2}"));
+
+	private static Map<String, ValidatorTypeCode> constants = new HashMap<String, ValidatorTypeCode>();
+
+    static {
+        for (ValidatorTypeCode c : values()) {
+            constants.put(c.value, c);
+        }
+    }
+
+    private final String value;
+    private final String errorCode;
+    private final MessageFormat messageFormat;
+    private final String errorCodeKey;
+
+    private ValidatorTypeCode(String value, String errorCode, MessageFormat messageFormat) {
+        this.value = value;
+        this.errorCode = errorCode;
+        this.messageFormat = messageFormat;
+        this.errorCodeKey = value + "ErrorCode";
+    }
+
+    public static List<ValidatorTypeCode> getNonFormatKeywords() {
+        final List<ValidatorTypeCode> result = new ArrayList<ValidatorTypeCode>();
+        for (ValidatorTypeCode keyword: values()) {
+            if (!FORMAT.equals(keyword)) {
+                result.add(keyword);
+            }
+        }
+        return result;
+    }
+
+    public static ValidatorTypeCode fromValue(String value) {
+        ValidatorTypeCode constant = constants.get(value);
+        if (constant == null) {
+            throw new IllegalArgumentException(value);
+        } else {
+            return constant;
+        }
+    }
+    
+    public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws Exception {
+            String shortClassName = getValue();
+            if (shortClassName.startsWith("$")) {
+                // remove "$" from class name for $ref schema
+                shortClassName = shortClassName.substring(1);
+            }
+
+            final String className = Character.toUpperCase(shortClassName.charAt(0)) + shortClassName.substring(1)
+                    + "Validator";
+            @SuppressWarnings("unchecked")
+            final Class<JsonValidator> clazz = (Class<JsonValidator>) Class
+                    .forName("com.networknt.schema." + className);
+            Constructor<JsonValidator> c = null;
+            c = clazz.getConstructor(
+                    new Class[] { String.class, JsonNode.class, JsonSchema.class, ValidationContext.class });
+            return c.newInstance(schemaPath + "/" + getValue(), schemaNode, parentSchema, validationContext);
+    }
+    
+    @Override
+    public String toString() {
+        return this.value;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public String getErrorCode() {
+        return errorCode;
+    }
+
+    public MessageFormat getMessageFormat() {
+        return messageFormat;
+    }
+
+    public String getErrorCodeKey() {
+        return errorCodeKey;
+    }
+
+}


Test patch:
diff --git a/src/test/resources/tests/optional/format.json b/src/test/resources/tests/optional/format.json
index 34236d3..99d1746 100644
--- a/src/test/resources/tests/optional/format.json
+++ b/src/test/resources/tests/optional/format.json
@@ -52,6 +52,66 @@
                 "description": "only RFC3339 not all of ISO 8601 are valid",
                 "data": "2013-350T01:01:01",
                 "valid": false
+            },
+            {
+                "description": "an invalid date-time string",
+                "data": "2019-02-29T08:30:06Z",
+                "valid": false
+            },
+            {
+                "description": "an invalid date-time string",
+                "data": "2019-02-28T13:30:06Z",
+                "valid": false
+            },
+            {
+                "description": "an invalid date-time string",
+                "data": "2019-02-28T08:60:06Z",
+                "valid": false
+            },
+            {
+                "description": "an invalid date-time string",
+                "data": "2019-02-28T08:30:60Z",
+                "valid": false
+            },
+            {
+                "description": "a valid date-time string",
+                "data": "2019-02-28T08:30:59.12345+08:00",
+                "valid": true
+            },
+            {
+                "description": "a valid date-time string",
+                "data": "2019-02-28T08:30:59.12345-12:00",
+                "valid": true
+            },
+            {
+                "description": "an invalid date-time string",
+                "data": "2019-02-28T08:30:59.12345-60:00",
+                "valid": false
+            },
+            {
+                "description": "an invalid date-time string",
+                "data": "2019-02-28T08:30:59.12345-12:60",
+                "valid": false
+            },
+            {
+                "description": "a valid date-time string",
+                "data": "2019-02-28T08:30:59.99999z",
+                "valid": true
+            },
+            {
+                "description": "an incomplete date-time string",
+                "data": "2019-02-28",
+                "valid": false
+            },
+            {
+                "description": "an incomplete date-time string",
+                "data": "2019-02-28-08:00",
+                "valid": false
+            },
+            {
+                "description": "an incomplete date-time string",
+                "data": "2019-02-28Z",
+                "valid": false
             }
         ]
     },
@@ -70,7 +130,12 @@
                 "valid": false
             },
             {
-                "description": "an invalid datee string",
+                "description": "an invalid date string",
+                "data": "2019-02-28T08:30:59.99999z",
+                "valid": false
+            },
+            {
+                "description": "a valid date string",
                 "data": "1963-12-31",
                 "valid": true
             },
@@ -78,6 +143,21 @@
                 "description": "only RFC3339 not all of ISO 8601 are valid",
                 "data": "2013-35",
                 "valid": false
+            },
+            {
+                "description": "an invalid date string",
+                "data": "2019-02-29",
+                "valid": false
+            },
+            {
+                "description": "an invalid date string",
+                "data": "2019-06-31",
+                "valid": false
+            },
+            {
+                "description": "a valid date string",
+                "data": "2048-02-29",
+                "valid": true
             }
         ]
     },