【Flask-RESTPlus系列】Part2:响应编组

时间:2023-12-24 23:30:25

0x00 内容概览

  1. 响应编组
    1. 基本使用
    2. 重命名属性
    3. 默认值
    4. 自定义字段及多值情况
    5. Url及其他具体字段
    6. 复杂结构
    7. 列表字段
    8. 嵌套字段
    9. api.model()工厂
    10. clone实现复制
    11. api.inherit实现多态
    12. 自定义字段
    13. 跳过值为None的字段
    14. 跳过嵌套字段中的None字段
    15. 使用JSON Schema定义模型
  2. 参考链接

0x01 响应编组(Response marshalling)

Flask-RESTPlus提供了一种便捷的方式来控制你在响应中实际渲染的数据,以及在输入载荷(payload)中所期望的数据。利用fields模块,你可以在响应中使用任何对象(ORM模块、自定义类等等)。另外,利用fields也能够实现格式化和过滤响应,这样我们就无需担心暴露内部数据结构的问题。

此外,还有一点好处是,可以很清晰地从你的代码中知道将会渲染什么数据,以及这些数据的格式、结构是怎样的。

1、基本使用

我们可以定义字段的一个字典或者有序字典,其中字典中的key为欲渲染对象的属性名或key,而对应的value则是一个将为该字段格式化并返回值的类。如下面代码所示,该例子中包含三个字段:两个是String类型、一个是格式化为ISO 8601时间字符串(也支持RFC 822)的DateTime类型,如下:

from flask_restplus import Resource, fields

model = api.model('Model', {
'name': fields.String,
'address': fields.String,
'date_updated': fields.DateTime(dt_format='rfc822'),
}) @api.route('/todo')
class Todo(Resource):
@api.marshal_with(model, envelope='resource')
def get(self, **kwargs):
return db_get_todo() # db_get_todo()为某个查询数据的函数

该例子假设你有一个自定义的数据库对象(todo),该对象拥有属性name、address和date_updated。而该对象的其他属性都是私有类型的,且不会在输出中进行渲染。另外,可选参数envelope用来指定封装输出结果。

装饰器marshal_with()接受你的数据对象,并对其按照model格式进行字段过滤。编组(marshalling)可以作用于单个对象、字典或者对象的列表。

注意:marshal_with()是一个很便捷的装饰器,它的作用等价于下面代码:

class Todo(Resource):
def get(self, **kwargs):
return marshal(db_get_todo(), model), 200

而@api.marshal_with装饰器则增加了swagger文档化功能。

2、重命名属性

大多数情况下,你的共有字段名与你内部的字段名都是不相同的。为了实现这一映射关系的配置,我们可以使用attribute参数:

model = {
'name': fields.String(attribute='private_name'),
'address': fields.String,
}

另外,attribute参数的值也可以指定为lambda表达式或者其他可调用的语句:

model = {
'name': fields.String(attribute=lambda x: x._private_name),
'address': fields.String,
}

此外,还可以利用attribute来访问嵌套的属性:

model = {
'name': fields.String(attribute='people_list.0.person_dictionary.name'),
'address': fields.String,
}

3、默认值

如果因为某个原因,你的数据对象中并不包含字段列表中的某个属性,那么我们就可以为该字段指定一个默认的返回值,从而避免返回None:

model = {
'name': fields.String(default='Anonymous User'),
'address': fields.String,
}

4、自定义字段及多值情况

有时候我们也有自定义格式的需求,此时我们可以让我们的类继承类fields.Raw,并实现format方法。当某个属性存储了多个片段的信息时,这一功能尤其方便。例如,一个bit字段的单个位能够代表不同的值。此时,你可以使用字段来乘以某个属性来来得到多个输出值。

下面示例假设flags属性中的第1个bit用来区分“Normal”和“Urgent”项,而第2个bit则用来区分“Read”和“Unread”。虽然这些项很容易存储在一个bit字段中,但是考虑到输出为了便于人们阅读,将它们分别转换成独立的字符串字段则更加优雅友好:

class UrgentItem(fields.Raw):
def format(self, value):
return "Urgent" if value & 0x01 else "Normal" class UnreadItem(fields.Raw):
def format(self, value):
return "Unread" if value & 0x02 else "Read" model = {
'name': fields.String,
'priority': UrgentItem(attribute='flags'),
'status': UnreadItem(attribute='flags'),
}

5、Url及其他具体字段

Flask-RESTPlus包含一个特殊字段fields.Url,它会为正被请求的资源生成一个URI。在为响应添加数据对象中不存在的数据时,这一点也是一个不错的示例:

class RandomNumber(fields.Raw):
def output(self, key, obj):
return random.random() model = {
'name': fields.String,
# todo_resource是我们调用api.route()时为某个资源指定的端点名
'uri': fields.Url('todo_resource'),
'random': RandomNumber,
}

默认情况下,fields.Url返回的是一个相对于根路径的相对URI。而为了生成包含schema(协议)、主机名和端口号的绝对URI,我们只需在字段声明中传入absolute=True的参数项。为了覆盖默认的schema,我们可以传入schema参数:

model = {
'uri': fields.Url('todo_resource', absolute=True)
'https_uri': fields.Url('todo_resource', absolute=True, scheme='https')
}

6、复杂结构

你可以提供一个扁平的结构,而marshal()则会按照定义的规则将其转换成一个嵌套结构:

>>> from flask_restplus import fields, marshal
>>> import json
>>>
>>> resource_fields = {'name': fields.String}
>>> resource_fields['address'] = {}
>>> resource_fields['address']['line 1'] = fields.String(attribute='addr1')
>>> resource_fields['address']['line 2'] = fields.String(attribute='addr2')
>>> resource_fields['address']['city'] = fields.String
>>> resource_fields['address']['state'] = fields.String
>>> resource_fields['address']['zip'] = fields.String
>>> data = {'name': 'bob', 'addr1': '123 fake street', 'addr2': '', 'city': 'New York', 'state': 'NY', 'zip': ''}
>>> json.dumps(marshal(data, resource_fields))
'{"name": "bob", "address": {"line 1": "123 fake street", "line 2": "", "state": "NY", "zip": "10468", "city": "New York"}}'

注意:上述示例中的address字段其实并不存在于数据对象中,但是任何子字段都能够直接从对象中访问该属性,就像它们并不是嵌套关系一样。

7、列表字段(List Field)

你也可以将字段解组成列表:

>>> from flask_restplus import fields, marshal
>>> import json
>>>
>>> resource_fields = {'name': fields.String, 'first_names': fields.List(fields.String)}
>>> data = {'name': 'Bougnazal', 'first_names' : ['Emile', 'Raoul']}
>>> json.dumps(marshal(data, resource_fields))
>>> '{"first_names": ["Emile", "Raoul"], "name": "Bougnazal"}'

8、嵌套字段(Nested Field)

既然嵌套字段使用字典可以将一个扁平数据对象转换成一个嵌套响应,那么你也可以使用Nested来将嵌套的数据结构解组,并对其进行适当的渲染:

>>> from flask_restplus import fields, marshal
>>> import json
>>>
>>> address_fields = {}
>>> address_fields['line 1'] = fields.String(attribute='addr1')
>>> address_fields['line 2'] = fields.String(attribute='addr2')
>>> address_fields['city'] = fields.String(attribute='city')
>>> address_fields['state'] = fields.String(attribute='state')
>>> address_fields['zip'] = fields.String(attribute='zip')
>>>
>>> resource_fields = {}
>>> resource_fields['name'] = fields.String
>>> resource_fields['billing_address'] = fields.Nested(address_fields)
>>> resource_fields['shipping_address'] = fields.Nested(address_fields)
>>> address1 = {'addr1': '123 fake street', 'city': 'New York', 'state': 'NY', 'zip': ''}
>>> address2 = {'addr1': '555 nowhere', 'city': 'New York', 'state': 'NY', 'zip': ''}
>>> data = {'name': 'bob', 'billing_address': address1, 'shipping_address': address2}
>>>
>>> json.dumps(marshal(data, resource_fields))
'{"billing_address": {"line 1": "123 fake street", "line 2": null, "state": "NY", "zip": "10468", "city": "New York"}, "name": "bob", "shipping_address": {"line 1": "555 nowhere", "line 2": null, "state": "NY", "zip": "10468", "city": "New York"}}'

该示例使用两个Nested字段。Nested构造函数接受一个字段组成的字典,然后将其渲染成一个子fields.input对象。Nested构造函数和嵌套字典(上个例子)之间的重要不同点是:属性的上下文环境。在本例中,billing_address是一个复杂的对象,它拥有自己的字段,而传入到嵌套字段中的上下文环境是子对象,而不是原始的data对象。也就是说:data.billing_address.addr1处于该范围,而在前一示例中,data.addr1则是位置属性。记住:Nested和List对象为属性创建了一个新的作用范围。

默认情况下,当子对象为None时,将会为嵌套字段生成一个包含默认值的对象,而不是null值。可以通过传入allow_null参数来修改这一点,查看Nested构造函数以了解更多信息。

使用Nested和List来编组更复杂对象的列表:

user_fields = api.model('User', {
'id': fields.Integer,
'name': fields.String,
}) user_list_fields = api.model('UserList', {
'users': fields.List(fields.Nested(user_fields)),
})

9、api.model()工厂

model()工厂允许我们实例化并注册模型到我们的API和命名空间(Namespace)中。如下所示:

my_fields = api.model('MyModel', {
'name': fields.String,
'age': fields.Integer(min=0)
}) # 等价于
my_fields = Model('MyModel', {
'name': fields.String,
'age': fields.Integer(min=0)
})
api.models[my_fields.name] = my_fields

10、clone实现复制

Model.clone()方法使得我们可以实例化一个增强模型,它能够省去我们复制所有字段的麻烦:

parent = Model('Parent', {
'name': fields.String
}) child = parent.clone('Child', {
'age': fields.Integer
})

Api/Namespace.clone也会将其注册到API。如下:

parent = api.model('Parent', {
'name': fields.String
}) child = api.clone('Child', parent, {
'age': fields.Integer
})

11、api.inherit实现多态

Model.inherit()方法允许我们以“Swagger”方式扩展模型,并开始解决多态问题:

parent = api.model('Parent', {
'name': fields.String,
'class': fields.String(discriminator=True)
}) child = api.inherit('Child', parent, {
'extra': fields.String
})

Api/Namespace.clone会将parent和child都注册到Swagger模型定义中:

parent = Model('Parent', {
'name': fields.String,
'class': fields.String(discriminator=True)
}) child = parent.inherit('Child', {
'extra': fields.String
})

本例中的class字段只有在其不存在于序列化对象中时,才会以序列化的模型名称进行填充。

Polymorph字段允许你指定Python类和字段规范的映射关系:

mapping = {
Child1: child1_fields,
Child2: child2_fields,
} fields = api.model('Thing', {
owner: fields.Polymorph(mapping)
})

12、自定义字段

自定义输出字段使得我们可以在无需直接修改内部对象的情况下,进行自定义的输出结果格式化操作。我们只需让类继承Raw,并实现format()方法:

class AllCapsString(fields.Raw):
def format(self, value):
return value.upper() # 使用示例
fields = {
'name': fields.String,
'all_caps_name': AllCapsString(attribute='name'),
}

也可以使用__schema_format__、__schema_type__和__schema_example__来指定生成的类型和例子:

class MyIntField(fields.Integer):
__schema_format__ = 'int64' class MySpecialField(fields.Raw):
__schema_type__ = 'some-type'
__schema_format__ = 'some-format' class MyVerySpecialField(fields.Raw):
__schema_example__ = 'hello, world'

13、跳过值为None的字段

我们可以跳过值为None的字段,而无需将这些字段编组为JSON值null。当你拥有很多值可能会为None的字段,而到底哪个字段的值为None又不可预测时,此时该特性在减小响应大小方面的优势就凸显出来了。

下面例子中,我们将可选参数skip_none设置为True:

>>> from flask_restplus import Model, fields, marshal_with
>>> import json
>>> model = Model('Model', {
... 'name': fields.String,
... 'address_1': fields.String,
... 'address_2': fields.String
... })
>>> @marshal_with(model, skip_none=True)
... def get():
... return {'name': 'John', 'address_1': None}
...
>>> get()
{'name', 'John'}

【Flask-RESTPlus系列】Part2:响应编组

可以看到,address_1和address_2被marshal_with()跳过了。address_1被跳过是因为它的值为None,而address_2被跳过是因为get()返回的字典中并不包含address_2这个key。

14、跳过嵌套字段中的None字段

如果你的模型使用了fields.Nested,那么你需要传递skip_none=True参数到fields.Nested中,只有这样该Nested字段中的子字段为None时才会被跳过:

>>> from flask_restplus import Model, fields, marshal_with
>>> import json
>>> model = Model('Model', {
... 'name': fields.String,
... 'location': fields.Nested(location_model, skip_none=True)
... })

15、使用JSON Schema定义模型

我们可以使用JSON Schema(Draft v4)来定义模型:

address = api.schema_model('Address', {
'properties': {
'road': {
'type': 'string'
},
},
'type': 'object'
}) person = address = api.schema_model('Person', {
'required': ['address'],
'properties': {
'name': {
'type': 'string'
},
'age': {
'type': 'integer'
},
'birthdate': {
'type': 'string',
'format': 'date-time'
},
'address': {
'$ref': '#/definitions/Address',
}
},
'type': 'object'
})

0x02 参考链接