如何使用Marshmallow序列化MongoDB ObjectId?

时间:2015-01-22 16:28:15

标签: python flask mongoengine marshmallow

我使用棉花糖和mongoengine在Flask顶部构建和使用API​​。当我打电话并且ID应该被序列化时,我收到以下错误:

TypeError: ObjectId('54c117322053049ba3ef31f3') is not JSON serializable

我看到了其他库的一些方法来覆盖ObjectId的处理方式。我还没想到Marshmallow,有人知道怎么做吗?

我的模特是:

class Process(db.Document):
    name = db.StringField(max_length=255, required=True, unique=True)
    created_at = db.DateTimeField(default=datetime.datetime.now, required=True)

我的序列化器:

class ProcessSerializer(Serializer):
    class Meta:
        fields = ("id", "created_at", "name")

观点:

class ProcessView(Resource):
    def get(self, id):
        process = Process.objects.get_or_404(id)
        return ProcessSerializer(process).data

4 个答案:

答案 0 :(得分:10)

当您将Meta.fields传递给架构时,Marshmallow会尝试为每个属性选择一个字段类型。由于它不知道ObjectId是什么,它只是将它传递给序列化的字典。当您尝试将其转储到JSON时,它不知道ObjectId是什么并引发错误。要解决这个问题,您需要告诉Marshmallow要用于id的字段。 BSON ObjectId可以转换为字符串,因此请使用String字段。

from marshmallow import Schema, fields

class ProcessSchema(Schema):
    id = fields.String()

    class Meta:
        additional =  ('created_at', 'name')

您还可以告诉Marshmallow ObjectId类型使用哪个字段,这样您就不必每次都添加字段。

from bson import ObjectId
from marshmallow import Schema, fields

Schema.TYPE_MAPPING[ObjectId] = fields.String

答案 1 :(得分:2)

marshmallow-mongoengine这样做:

  

Marshmallow-Mongoengine是将Mongoengine文档与Marshmallow Schema合并在一起。

import marshmallow_mongoengine as ma


class ProcessSchema(ma.ModelSchema):
    class Meta:
        model = Process

它有一个ObjectId字段,用于序列化/反序列化ObjectId

答案 2 :(得分:0)

您可以将fields.Field类扩展为create your own fieldHere's how marshmallow-mongoengine(在另一个答案中提到)实现了以下目的:

[@]

然后:

/#/--exclude=

(我发现当不使用MongoEngine时,仅使用pymongo,这很有用)

答案 3 :(得分:0)

类似于上述@dcollien,我扩展了field.Field并使用助手创建了自己的自定义字段,类似于棉花糖在内部处理字段类型的方式:

from marshmallow import fields, missing
from marshmallow.exceptions import ValidationError
from bson.objectid import ObjectId
from bson.errors import InvalidId
import json

def oid_isval(val: Any) -> bool:
    """
    oid_isval [summary]

    Parameters
    ----------
    val : {Any}
        Value to be assessed if its an ObjectId

    Returns
    ----------
    val : bool
        True if val is an ObjectId, otherwise false
    """    
    if ObjectId.is_valid(val):
        return val
def ensure_objid_type(val: Union[bytes, str, ObjectId]) -> ObjectId:
    """
    Ensures that the value being passed is return as an ObjectId and is a valid ObjectId

    Parameters
    ----------
    val : Union[bytes, str, ObjectId]
        The value to be ensured or converted into an ObjectId and is a valid ObjectId

    Returns
    ----------
    val : ObjectId
        Value of type ObjectId
        
    Raises
    ----------
    ValidationError: Exception
        If it's not an ObjectId or can't be converted into an ObjectId, raise an error.
            
    """
    try:
        # If it's already an ObjectId and it's a valid ObjectId, return it
        if isinstance(val, ObjectId) and oid_isval(val):
            logger.info(f"It's an ObjectId and it's valid! = {val}")
            return val
        
        # Otherwise, if it's a bytes object, decode it and turn it into a string
        elif isinstance(val, bytes):
            val = ObjectId(str(val.decode("utf-8")))
            logger.info(f"Decoded and converted bytes object to ObjectId! = {val}")

        # Otherwise, if it's a string, turn it into an ObjectId and check that it's valid 
        elif isinstance(val, str):
            val = ObjectId(val)
            logger.info(f"Converted str to ObjectId! = {val}")
        
        # Check to see if the converted value is a valid objectId
        if oid_isval(val):
            logger.info(f"It's a valid ObjectId! = {val}")
            return val
    except InvalidId as error:
        logger.error(f"Not a valid ObjectId = {val} | error = {error}")
        raise ValidationError(json.loads(json.dumps(f"{error}")))

class ObjectIdField(fields.Field):
    """Custom field for ObjectIds."""
    # Default error messages
    default_error_messages = {
        "invalid_ObjectId": "Not a valid ObjectId."
    }

    def _serialize(self, value, attr, obj, **kwargs) -> Optional[ObjectId]:
        if value is None:
            return None
        return ensure_objid_type(value)

    def _deserialize(self, value, attr, data, **kwargs):
        if value is None:
            return missing
        if not isinstance(value, (ObjectId, str, bytes)):
            raise self.make_error("_deserialize: Not a invalid ObjectId")
        try:
            return ensure_objid_type(value)
        except UnicodeDecodeError as error:
            raise self.make_error("invalid_utf8") from error
        except (ValueError, AttributeError, TypeError) as error:
            raise ValidationError("ObjectIds must be a 12-byte input or a 24-character hex string") from error