Repository: marshmallow-code/marshmallow

Issue:
fields.Date accepts input in non ISO8601 format. This leads to unaccepted behaviour.
The doc of `fields.Date` suggests that it accepts only ISO8601 date format.

```python
class Date(Field):
    """ISO8601-formatted date string.
    :param kwargs: The same keyword arguments that :class:`Field` receives.
    """
```

It works with ISO8601 format fine:
```python
In [2]: fields.Date().deserialize('2018-10-08')
Out[2]: datetime.date(2018, 10, 8)
```

However unfortunately it also works with non ISO8601 formats:
```python
In [3]: fields.Date().deserialize('08-10-2018')
Out[3]: datetime.date(2018, 8, 10)
```
And in this case it seems like it parses the format as `%m-%d-%Y`
I feel, it should raise a ValidationError with `invalid_date` format incase the input is not in the iso8601 format.

Digging further into the issue I found that DateField uses `utils.from_iso_date`; which inturn uses dateutil (if available) parser to parse the date.
I think that util should be modified to use [dateutil.parser.isoparse](https://dateutil.readthedocs.io/en/stable/parser.html#dateutil.parser.isoparse)


Discussion:
dushr: 👍 

Patch:
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 03dc84b3..5c44518e 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -107,3 +107,4 @@ Contributors (chronological)
 - Maxim Novikov `@m-novikov <https://github.com/m-novikov>`_
 - James Remeika `@remeika <https://github.com/remeika>`_
 - Karandeep Singh Nagra `@knagra <https://github.com/knagra>`_
+- Dushyant Rijhwani `@dushr<https://github.com/dushr>`_
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 5e3a3449..eb103385 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -10,6 +10,8 @@ Features:
 - *Backwards-incompatible*: Rename ``DateTime``'s ``dateformat`` Meta option
   to ``datetimeformat``. ``dateformat`` now applies to ``Date`` (:pr:`869`).
   Thanks :user:`knagra` for implementing these changes.
+- Enforce ISO 8601 when deserializing date and time (:issue:`899`).
+  Thanks :user:`dushr` for the report and the work on the PR.
 
 3.0.0b16 (2018-09-20)
 +++++++++++++++++++++
diff --git a/marshmallow/utils.py b/marshmallow/utils.py
index 0090e20c..c3d7b01e 100755
--- a/marshmallow/utils.py
+++ b/marshmallow/utils.py
@@ -224,17 +224,25 @@ def rfcformat(dt, localtime=False):
 
 
 # From Django
-_iso8601_re = re.compile(
+_iso8601_datetime_re = re.compile(
     r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})'
     r'[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
     r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?'
     r'(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$',
 )
 
+_iso8601_date_re = re.compile(
+    r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$',
+)
+
+_iso8601_time_re = re.compile(
+    r'(?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
+    r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?',
+)
+
 
 def isoformat(dt, localtime=False, *args, **kwargs):
-    """Return the ISO8601-formatted UTC representation of a datetime object.
-    """
+    """Return the ISO8601-formatted UTC representation of a datetime object."""
     if localtime and dt.tzinfo is not None:
         localized = dt
     else:
@@ -275,25 +283,27 @@ def from_iso(datestring, use_dateutil=True):
     return from_iso_datetime(datestring, use_dateutil)
 
 
-def from_iso_datetime(datestring, use_dateutil=True):
+def from_iso_datetime(datetimestring, use_dateutil=True):
     """Parse an ISO8601-formatted datetime string and return a datetime object.
 
     Use dateutil's parser if possible and return a timezone-aware datetime.
     """
-    if not _iso8601_re.match(datestring):
+    if not _iso8601_datetime_re.match(datetimestring):
         raise ValueError('Not a valid ISO8601-formatted datetime string')
     # Use dateutil's parser if possible
     if dateutil_available and use_dateutil:
-        return parser.parse(datestring)
+        return parser.isoparse(datetimestring)
     else:
         # Strip off timezone info.
-        return datetime.datetime.strptime(datestring[:19], '%Y-%m-%dT%H:%M:%S')
+        return datetime.datetime.strptime(datetimestring[:19], '%Y-%m-%dT%H:%M:%S')
 
 
 def from_iso_time(timestring, use_dateutil=True):
     """Parse an ISO8601-formatted datetime string and return a datetime.time
     object.
     """
+    if not _iso8601_time_re.match(timestring):
+        raise ValueError('Not a valid ISO8601-formatted time string')
     if dateutil_available and use_dateutil:
         return parser.parse(timestring).time()
     else:
@@ -304,8 +314,10 @@ def from_iso_time(timestring, use_dateutil=True):
         return datetime.datetime.strptime(timestring, fmt).time()
 
 def from_iso_date(datestring, use_dateutil=True):
+    if not _iso8601_date_re.match(datestring):
+        raise ValueError('Not a valid ISO8601-formatted date string')
     if dateutil_available and use_dateutil:
-        return parser.parse(datestring).date()
+        return parser.isoparse(datestring).date()
     else:
         return datetime.datetime.strptime(datestring[:10], '%Y-%m-%d').date()
 


Test patch:
diff --git a/tests/test_deserialization.py b/tests/test_deserialization.py
index a6b2fb6d..78ff0751 100644
--- a/tests/test_deserialization.py
+++ b/tests/test_deserialization.py
@@ -377,6 +377,8 @@ class TestFieldDeserialization:
             '',
             [],
             '2018-01-01',
+            dt.datetime.now().strftime('%H:%M:%S %Y-%m-%d'),
+            dt.datetime.now().strftime('%m-%d-%Y %H:%M:%S'),
         ],
     )
     def test_invalid_datetime_deserialization(self, in_value):
@@ -569,6 +571,7 @@ class TestFieldDeserialization:
             '',
             123,
             [],
+            dt.date(2014, 8, 21).strftime('%d-%m-%Y'),
         ],
     )
     def test_invalid_date_field_deserialization(self, in_value):
diff --git a/tests/test_options.py b/tests/test_options.py
index 409e4ae7..834bb106 100644
--- a/tests/test_options.py
+++ b/tests/test_options.py
@@ -128,7 +128,7 @@ class TestFieldOrdering:
             'created': dt.datetime.now().isoformat(),
             'id': 123,
             'homepage': 'http://foo.com',
-            'birthdate': dt.datetime.now().isoformat(),
+            'birthdate': dt.datetime.now().date().isoformat(),
         }})
         user_data = data['user']
         keys = list(user_data)