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

 

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也可以实现格式化和过滤响应,这样咱们就无需担忧暴露内部数据结构的问题。html

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

一、基本使用

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

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用来指定封装输出结果。flask

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

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

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

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

二、重命名属性

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

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来访问嵌套的属性:ui

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

三、默认值

若是由于某个缘由,你的数据对象中并不包含字段列表中的某个属性,那么咱们就能够为该字段指定一个默认的返回值,从而避免返回None:

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

四、自定义字段及多值状况

有时候咱们也有自定义格式的需求,此时咱们可让咱们的类继承类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'),
}

五、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')
}

六、复杂结构

 你能够提供一个扁平的结构,而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': '10468'}
>>> json.dumps(marshal(data, resource_fields))
'{"name": "bob", "address": {"line 1": "123 fake street", "line 2": "", "state": "NY", "zip": "10468", "city": "New York"}}'

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

七、列表字段(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"}'

八、嵌套字段(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': '10468'}
>>> address2 = {'addr1': '555 nowhere', 'city': 'New York', 'state': 'NY', 'zip': '10468'}
>>> 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)),
})

九、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

十、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
})

十一、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)
})

十二、自定义字段

自定义输出字段使得咱们能够在无需直接修改内部对象的状况下,进行自定义的输出结果格式化操做。咱们只需让类继承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'

1三、跳过值为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'}

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

1四、跳过嵌套字段中的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)
... })

1五、使用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 参考连接

相关文章
相关标签/搜索