1. Tutorial

Marshmallow-Mongoengine is about bringing together a Mongoengine Document with a Marshmallow Schema.

1.1. Warming up

First we need a Mongoengine Document:

import mongoengine as me


class Task(me.EmbeddedDocument):
    content = me.StringField(required=True)
    priority = me.IntField(default=1)


class User(me.Document):
    name = me.StringField()
    password = me.StringField(required=True)
    email = me.StringField()
    tasks = me.ListField(me.EmbeddedDocumentField(Task))

Great ! Now it’s time for the Marshmallow Schema. To keep things DRY, we use marshmallow-mongoengine to do the mapping:

import marshmallow_mongoengine as ma


class UserSchema(ma.ModelSchema):
    class Meta:
        model = User

Finally it’s time to use our schema to load/dump documents:

>>> user_schema = UserSchema()
>>> u = user_schema.load({
...     "name": "John Doe", "email": "jdoe@example.com", "password": "123456",
...     "tasks": [{"content": "Find a proper password"}]})
>>> u.save()
<User: User object>
>>> u.name
"John Doe"
>>> u.tasks
[<Task: Task object>]
>>> user_schema.dump(u)
{"name": "John Doe", "email": "jdoe@example.com", "password": "123456", "tasks": [{"content": "Find a proper password", "priority": 1}]}

If the document already exists, we can update it using update

>>> u
>>> u2 = user.schema.update(u, {"name": "Jacques Faite"})
>>> u2 is u
True
>>> u2.name
"Jacques Faite"

Note

required argument in the fields is not taken into account when using update

1.2. Configuring the schema

Let say we use user_schema.dump to send data to a client throught HTTP. In this case, returning the password field seems a pretty bad idea !

We could solve this by using marshmallow’s Meta.exclude list, but this means the field would be also excluded loading.

The solution is to use the Model.model_fields_kwargs option for customizing the field (de)serializers:

class UserSchemaNoPassword(ma.ModelSchema):
    class Meta:
        model = User
        model_fields_kwargs = {'password': {'load_only': True}}

Now consider the loading process: For the moment we directly put the password in the password field. This is not a good idea - it is also a really poor idea to use “123456” as the password ;-) - we should first hash and salt it.

To do that, we need to disable the build of the Mongoengine document by specifying Model.model_build_obj

class UserSchemaJSON(ma.ModelSchema):
    class Meta:
        model = User
        model_build_obj = False # default is True

Now the schema will do all the integrity checks, but after that will stop and return a dict:

>>> user_schema = UserSchemaJSON()
>>> data = user_schema.load({"name": "John Doe", "email": "jdoe@example.com", "password": "123456"})
>>> data
{"name": "John Doe", "email": "jdoe@example.com", "password": "123456"}
>>> data["password"] = hash_and_salt(data["password"]) # Alter the data
>>> User(**data) # Finally build the Mongoengine document from the data
<User: User object>

1.3. Customizing the schema

Now let’s say we want to customize the way the tasks are dumped. For example we want to return the field priority in a more understandable way than just a number (1 => “High”, 2 => “Medium”, 3 => “Will see tomorrow”).

Given that we can shadow the auto-generated fields by defining our own in the schema, we only have to redefine the property field and we’re done !

class UserSchemaCustomPriority(ma.ModelSchema):
    class Meta:
        model = User

    priority = ma.fields.Method(serialize="_priority_serializer", deserialize="_priority_deserializer")

    def _priority_serializer(self, obj):
        if obj.priority == 1:
            return "High"
        elif obj.priority == 2:
            return "Medium"
        else:
            return "Will do tomorrow"
>>> user_schema = UserSchemaCustomPriority()
>>> user = User(name="John Doe", email="jdoe@example.com",
...             tasks=[{"content": "Find a proper password"},
...                    {"content": "Learn to cook", "priority": 2},
...                    {"content": "Fix issues", "priority": 3}])
>>> dump = user_schema.dump(user)
>>> dump
{"name": "John Doe", "email": "jdoe@example.com", "tasks": [{"content": "Find a proper password", "priority": "High"}, {"content": "Learn to cook", "priority": "Medium"}, {"content": "Fix issues", "priority": "Will do tomorrow"}]}