Repository: MongoEngine/mongoengine

Issue:
DictField validate failed without default connection
If only use `db_alias` without setting `default connect`, `DictField` validate failed.
```python
class TestDict(Document):
    data = DictField()

    meta = {
        'db_alias': 'alias_1'
    }

d = TestDict(data={'a': 1})
d.save()
```
Exception:
```python
mongoengine.connection.ConnectionFailure: You have not defined a default connection
```

Cause:
https://github.com/MongoEngine/mongoengine/blob/1cc20c97707c722c34c7bd6deec2a0aad406fd34/mongoengine/fields.py#L1091
```python
    def validate(self, value):
        """Make sure that a list of valid fields is being used."""
        if not isinstance(value, dict):
            self.error("Only dictionaries may be used in a DictField")

        if key_not_string(value):
            msg = "Invalid dictionary key - documents must have only string keys"
            self.error(msg)

        curr_mongo_ver = get_mongodb_version()

        if curr_mongo_ver < MONGODB_36 and key_has_dot_or_dollar(value):
            self.error(
                'Invalid dictionary key name - keys may not contain "."'
                ' or startswith "$" characters'
            )
        elif curr_mongo_ver >= MONGODB_36 and key_starts_with_dollar(value):
            self.error(
                'Invalid dictionary key name - keys may not startswith "$" characters'
            )
        super(DictField, self).validate(value)
```
DictField need to use `default` connection to check mongodb version.


Discussion:
argh, Good catch, that's a bug. I'll look into this shortly

Resolving patch:
diff --git a/mongoengine/fields.py b/mongoengine/fields.py
--- a/mongoengine/fields.py
+++ b/mongoengine/fields.py
@@ -1088,14 +1088,12 @@ def validate(self, value):
             msg = "Invalid dictionary key - documents must have only string keys"
             self.error(msg)
 
-        curr_mongo_ver = get_mongodb_version()
-
-        if curr_mongo_ver < MONGODB_36 and key_has_dot_or_dollar(value):
-            self.error(
-                'Invalid dictionary key name - keys may not contain "."'
-                ' or startswith "$" characters'
-            )
-        elif curr_mongo_ver >= MONGODB_36 and key_starts_with_dollar(value):
+        # Following condition applies to MongoDB >= 3.6
+        # older Mongo has stricter constraints but
+        # it will be rejected upon insertion anyway
+        # Having a validation that depends on the MongoDB version
+        # is not straightforward as the field isn't aware of the connected Mongo
+        if key_starts_with_dollar(value):
             self.error(
                 'Invalid dictionary key name - keys may not startswith "$" characters'
             )
diff --git a/mongoengine/mongodb_support.py b/mongoengine/mongodb_support.py
--- a/mongoengine/mongodb_support.py
+++ b/mongoengine/mongodb_support.py
@@ -11,7 +11,7 @@
 
 
 def get_mongodb_version():
-    """Return the version of the connected mongoDB (first 2 digits)
+    """Return the version of the default connected mongoDB (first 2 digits)
 
     :return: tuple(int, int)
     """


Test patch:
diff --git a/tests/fields/test_dict_field.py b/tests/fields/test_dict_field.py
--- a/tests/fields/test_dict_field.py
+++ b/tests/fields/test_dict_field.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 import pytest
+from bson import InvalidDocument
 
 from mongoengine import *
 from mongoengine.base import BaseDict
@@ -19,22 +20,24 @@ class BlogPost(Document):
         post = BlogPost(info=info).save()
         assert get_as_pymongo(post) == {"_id": post.id, "info": info}
 
-    def test_general_things(self):
-        """Ensure that dict types work as expected."""
+    def test_validate_invalid_type(self):
+        class BlogPost(Document):
+            info = DictField()
+
+        BlogPost.drop_collection()
+
+        invalid_infos = ["my post", ["test", "test"], {1: "test"}]
+        for invalid_info in invalid_infos:
+            with pytest.raises(ValidationError):
+                BlogPost(info=invalid_info).validate()
 
+    def test_keys_with_dots_or_dollars(self):
         class BlogPost(Document):
             info = DictField()
 
         BlogPost.drop_collection()
 
         post = BlogPost()
-        post.info = "my post"
-        with pytest.raises(ValidationError):
-            post.validate()
-
-        post.info = ["test", "test"]
-        with pytest.raises(ValidationError):
-            post.validate()
 
         post.info = {"$title": "test"}
         with pytest.raises(ValidationError):
@@ -48,25 +51,34 @@ class BlogPost(Document):
         with pytest.raises(ValidationError):
             post.validate()
 
-        post.info = {1: "test"}
-        with pytest.raises(ValidationError):
-            post.validate()
-
         post.info = {"nested": {"the.title": "test"}}
         if get_mongodb_version() < MONGODB_36:
-            with pytest.raises(ValidationError):
-                post.validate()
+            # MongoDB < 3.6 rejects dots
+            # To avoid checking the mongodb version from the DictField class
+            # we rely on MongoDB to reject the data during the save
+            post.validate()
+            with pytest.raises(InvalidDocument):
+                post.save()
         else:
             post.validate()
 
         post.info = {"dollar_and_dot": {"te$st.test": "test"}}
         if get_mongodb_version() < MONGODB_36:
-            with pytest.raises(ValidationError):
-                post.validate()
+            post.validate()
+            with pytest.raises(InvalidDocument):
+                post.save()
         else:
             post.validate()
 
-        post.info = {"title": "test"}
+    def test_general_things(self):
+        """Ensure that dict types work as expected."""
+
+        class BlogPost(Document):
+            info = DictField()
+
+        BlogPost.drop_collection()
+
+        post = BlogPost(info={"title": "test"})
         post.save()
 
         post = BlogPost()