序列化(Serialization)与反序列化(Deserialization)是RESTful API 开发中绕不开的一环,开发时,序列化与反序列化的功能实现中通常也会包含数据校验(Validation)相关的业务逻辑。本文结合我的实践经验,介绍一种Flask RESTful API开发中实现序列化和反序列化的方法,如果想了解更多相关的理论要点,可以参考Gevin的博客之前的文章。
1. Flask 相关功能基础
python 内置数据类型中的dict和list,可以直接序列化为文本,如:
# Dict和list能够直接被序列化为文本,如下:
def test_list():
data = [{'a':1, 'b':2}, {'c':3, 'd':4}]
return jsonify(result=data)
def test_dict1():
data = {'a':1, 'b':2, 'c':3}
return jsonify(data)
def test_dict2():
data = {'a':1, 'b':2, 'c':3}
return jsonify(**data)
def test_dict3():
data = {'a':1, 'b':2, 'c':3}
return jsonify(result=data)
其中,jsonify
的作用是,把dict
或list
转换为string
(类似于json.dumps()
),同时,在response的header中,添加content-type =application/json
,由于RESTful API通常以json
格式作为载体,该方法算是一个shortcut。
由此,对象的序列化,只要实现object -> dict
或 objects -> list
即可。
反序列化,通常分两步实现:
- 将request中的文本数据转换为python原生的dict或list
- 基于dict为初始化数据,创建类的实例
反序列化实现的关键是上面第一步,而这一步实现非常容易,如果数据载体为 json
,只需调用 request.get_json()
方法,即可将传入的文本数据转换为dict或list。
2. Marshmallow
Marshmallow 是一个强大的轮子,很好的实现了 object -> dict
, objects -> list
, string -> dict
和 string -> list
。
Marshmallow is an ORM/ODM/framework-agnostic library for converting complex datatypes, such as objects, to and from native Python datatypes.
-- From marshmallow官方文档
Marshmallow的使用,将从下面几个方面展开,在开始之前,首先需要一个用于序列化和反序列化的类,我直接与marshmallow官方文档保持一致了:
class User(object):
def __init__(self, name, email):
self.name = name
self.email = email
self.created_at = dt.datetime.now()
Schema
要对一个类(记为Class_A
,以便表达)进行序列化和反序列化,首先要创建一个与之对应的类(记Class_A'
),负责实现Class_A
的序列化、序列化和数据校验等,Class_A'
就是schema,即:
Schema是序列化功能的载体,每个需要被序列化或反序列化的类,都要设计一个相应的Schema,以实现相关功能。Schema中的字段,是被序列化的类的映射,如:
class UserSchema(Schema):
name = fields.Str()
email = fields.Email()
created_at = fields.DateTime()
创建schema
时,schema中的字段必须与类的成员命名一致,不一致的字段无法被序列化和反序列化。
序列化
序列化使用schema中的dump()
或dumps()
方法,其中,dump()
方法实现obj -> dict
,dumps()
方法实现 obj -> string
,由于Flask能直接序列化dict,所以通常Flask与Marshmallow配合序列化时,用 dump()
方法即可。
from marshmallow import pprint
user = User(name="Monty", email="monty@python.org")
schema = UserSchema()
result = schema.dump(user)
pprint(result.data)
json_result = schema.dumps(user)
pprint(json_result.data)
反序列化
反序列化基于schema中的load()
或loads()
方法,默认情况下,load()
方法将一个传入的dict
,结合schema的约定,再转换为一个dict
,而loads()
方法的传入参数是json格式的string
,同样将传入数据转换为符合规范的dict
。由于调用load()
或loads()
方法时,会执行下面提到的数据校验
,所以在开发RESTful API时,对传入数据执行load()
或loads()
方法是必要的。load()
方法使用如下:
from pprint import pprint
user_data = {
'created_at': '2014-08-11T05:26:03.869245',
'email': u'ken@yahoo.com',
'name': u'Ken'
}
schema = UserSchema()
result = schema.load(user_data)
pprint(result.data)
对反序列化而言,将传入的dict
变成object
更加有意义。在Marshmallow中,dict -> object
的方法需要自己实现,然后在该方法前面加上一个decoration:post_load
即可,即:
from marshmallow import Schema, fields, post_load
class UserSchema(Schema):
name = fields.Str()
email = fields.Email()
created_at = fields.DateTime()
@post_load
def make_user(self, data):
return User(**data)
这样每次调用load()
方法时,会按照make_user
的逻辑,返回一个User类
对象。
user_data = {
'name': 'Ronnie',
'email': 'ronnie@stones.com'
}
schema = UserSchema()
result = schema.load(user_data)
result.data # => <User(name='Ronnie')>
Objects <-> List
上面的序列化和反序列化,是针对一个object而言的,对于objects的处理,只需在schema中增加一个参数:many=True
,即:
user1 = User(name="Mick", email="mick@stones.com")
user2 = User(name="Keith", email="keith@stones.com")
users = [user1, user2]
# option 1:
schema = UserSchema(many=True)
result = schema.dump(users)
# Option 2:
schema = UserSchema()
result = schema.dump(users, many=True)
result.data
Validation
Marshmallow中的Validation功能用于校验客户端传入的数据是否规范,通常用于创建和修改数据。
Validation可分为 field level validation
和 schema level validation
,创建schema时,实现必要Validation是必须的,由于详细阐述占用的篇幅会比较长,这部分内容请大家直接查看官方文档:
Partial Loading
按照RESTful架构风格的要求,更新数据使用HTTP方法中的PUT
或PATCH
方法,使用PUT
方法时,需要把完整的数据全部传给服务器,使用PATCH
方法时,只需把需要改动的部分数据传给服务器即可。因此,当使用PATCH
方法时,传入数据存在无法通过Marshmallow 数据校验的风险,为了避免这种情况,需要借助Partial Loading功能。
实现Partial Loadig只要在schema中增加一个partial
参数即可:
class UserSchema(Schema):
name = fields.String(required=True)
age = fields.Integer(required=True)
data, errors = UserSchema().load({'age': 42}, partial=True)
# OR UserSchema(partial=True).load({'age': 42})
data, errors # => ({'age': 42}, {})
Context
关于Marshmallow,最后想提的一点是,context
:
The context attribute of a Schema is a general-purpose store for extra information that may be needed for (de)serialization. It may be used in both Schema and Field methods.
Context
是很用的,比如,更新数据时,更新的是一个数据库中已有的对象,反序列化操作如果能基于该对象来反序列化就再好不过了,实现这个小需求一种可行方案是,把该对象放到context中,schema中针对该对象实现相关业务逻辑。
3. Marshmallow 与 Flask 的结合
Marshmallow有一个flask extention:flask-marshmallow,该扩展与Flask-SQLAlchemy
和marshmallow-sqlalchemy
有集成,有兴趣的同学不妨研究一下。
对Gevin而言,数据库抽象层通常使用Flask-MongoEngine,marshmallow本身就够用了。
Talk is cheap, show me the code.
关于Marshmallow在Flask中的应用,大家可以查看我在GitHub上的这两个代码文件:
以上两个代码文件来自我尚在开发中的项目JuneMessage,RESTful API和schema部分已经完成,可以参考一下。另外,schema部分不仅把上面提到的要点均实现了一遍,在代码的组织上也更进一步,看代码比单纯看上面的文字应该更有意义。
注:
Flask 功能基础部分,附加一个说明。
需要注意的是,处于安全考虑,Flask不允许直接返回list
,即:
data = [{'a':1, 'b':2}, {'c':3, 'd':4}]
# This is OK
@app.route('/list1/')
def test_list():
return jsonify(result=data)
# This fails
@app.route('/list1/')
def test_list():
return jsonify(data)
由于这段内容不是本文主题,所以放到最后做补充说明。
RESTful 架构风格相关文章:
注:转载本文,请与Gevin联系
欢迎关注我的微信公众账号
