Flask-RESTful - 不要返回对象属性,而应返回null

19

假设我有一个客户表,其中包含idnameemail字段。其中email字段是可选的

代码如下:

client_fields = {
   'id' : fields.String,
   'name' : fields.String,
   'email' : fields.String
}

并用于显示:

class ClientList(Resource):
    @marshal_with(client_fields)
    def get(self):
       return model.Client.query.all()

当未提供电子邮件时,API 返回如下的 JSON:

{
   "id": "1",
   "name": "John Doe",
   "email": null
}

但是我希望它返回这个对象:

{
   "id": "1",
   "name": "John Doe"
}

这基本上意味着,我不想要一个空值的属性,而是希望它根本不返回这样的属性。有办法实现这个吗?


16
这通常不是一个好的设计决策。对于那些正在寻找电子邮件字段的API使用者,现在需要特殊处理以处理该字段是否存在。相比可能存在或不存在的字段,为使用者提供空字段更加安全。 - kylieCatt
我并不完全同意这个观点。对象可以来自多个数据源,并且可能有大量不完整的数据。在具体情况下,有时这是一个完全合理的决定。 - Magoo
3个回答

13

我会使用 marshal 函数而不是 marshal_with 装饰器:

class ClientList(Resource):
    def get(self):
       clients = []
       for client in model.Client.query.all():
           if client.email:
               clients.append(marshal(client_fields))
           else:
               clients.append(marshal(client_fields_no_email))
       return clients

或者更好的是

class ClientList(Resource):
    def get(self):
       return [client_marshal(client) for client in model.Client.query.all()]

随着

def client_marshal(client):
    if client.email:
        return {'id' : fields.String,
                'name' : fields.String,
                'email' : fields.String}
    else:
        return {'id' : fields.String,
                'name' : fields.String}

4
如果可能的修改数量相对较小,这种方法是可行的。但是如果您想根据OAuth范围、用户隐私设置等显示不同的字段集合,该怎么办呢?请注意,不要改变原意。 - bigblind
1
@bigblind 是的,我来这里寻求许多解决方案之一。 - Tommy

11

有两种方法可以实现,即预编排和后编排修改。 预编排会删除客户端字段dict中字段名称的任何默认值,但后编排会保留它们。

在预编排方法中,如果客户端的电子邮件为None,则必须将修改后的字段dict传递给marshal函数。
例如;


import json
from flask_restful import fields, marshal, marshal_with

class Client(object):
    def __init__(self, id, name, email=None):
        self.id = id
        self.name = name
        self.email = email

client_fields = {
    'id': fields.String,
    'name': fields.String,
    'email': fields.String
}

def get():
    clients =[Client(1, 'Tom'), Client(2, 'John', 'john@example.com')]
    return [marshal(client, client_fields if client.email else {k: v for k, v in client_fields.items() if k != 'email'}) for client in clients]

print(json.dumps(get()))

输出;

[{"id": "1", "name": "Tom"}, {"email": "john@example.com", "id": "2", "name": "John"}]

在后序编组中,如果marshal_with返回的OrderedDict的电子邮件字段为None,则必须将其删除。
默认情况下,de_none函数会删除所有None字段,如果不希望这样做,则必须显式传递字段名称,并且如果marshal_with接受相同的参数,则还必须传递envelope参数。

from functools import wraps
import json
from flask_restful import fields, marshal, marshal_with

client_fields = {
    'id': fields.String,
    'name': fields.String,
    #'email': fields.String(default='user@example.com')
    'email': fields.String
}

class Client(object):
    def __init__(self, id, name, email=None):
        self.id = id
        self.name = name
        self.email = email

def de_none(envelope=None, *fields):
    def decorator(func):
        def dict_remove(d):
            if fields:
                for field in fields:
                    if d[field] is None:
                        d.pop(field)
            else:
                for k, v in d.items():
                   if v is None:
                       d.pop(k)

        @wraps(func)
        def decorated(*args, **kwargs):
            data = result = func(*args, **kwargs)
            if isinstance(result, tuple):
                data = result[0]
            if envelope:
                data = data[envelope]
            if isinstance(data, (list, tuple)):
                for d in data:
                    dict_remove(d)
            else:
                dict_remove(data)
            return result
        return decorated
    return decorator

@de_none()
@marshal_with(client_fields)
def get():
    #return [Client(1, 'Tom'), Client(2, 'john', 'john@example.com')], 200, {'Etag': 'blah'}
    #return [Client(1, 'Tom'), Client(2, 'john', 'john@example.com')]
    #return Client(1, 'Tom'), 200, {'Etag': 'foo'}
    return Client(1, 'Tom')

print(json.dumps(get()))


@de_none()
@marshal_with(client_fields)
def get():
    return Client(2, 'John', 'john@example.com'), 201, {'Etag': 'ok'}

print(json.dumps(get()))

输出;

{"id": "1", "name": "Tom"}
{"email": "john@example.com", "id": "2", "name": "John"}

更新 请求钩子
可以使用app.after_request修饰符来修改响应对象。任何给定字段的默认值都将被保留。
remove_none_fields请求钩子需要fields参数,该参数可以是None来移除所有具有None值的字段,或者是一个字段名称列表,以选择性地移除。

import json
from flask import Flask, Response
from flask_restful import fields, marshal_with, Api, Resource

app = Flask(__name__)
api = Api(app)

class Client(object):
    def __init__(self, id, name, email=None):
        self.id = id
        self.name = name
        self.email = email

client_fields = {
    'id': fields.String,
    'name': fields.String,
    'email': fields.String,
    'age': fields.String
}

class ClientList(Resource):
    @marshal_with(client_fields)
    def get(self):
        clients =[Client(1, 'Tom'), Client(2, 'John', 'john@example.com')]
        return clients, 200

@app.after_request
def remove_none_fields(resp, fields=('email',)):
    """
    removes all None fields
    """

    if not 'application/json' in resp.content_type:
        return resp

    def dict_remove(d, fields):
        if fields:
            for field in fields:
                if d[field] is None:
                    d.pop(field)
        else:
            for k, v in tuple(d.items()):
                if v is None:
                    d.pop(k)

    data = json.loads(resp.get_data())
    if isinstance(data, list):
        for obj in data:
            dict_remove(obj, fields)
    else:
        dict_remove(data, fields)

    resp.set_data(json.dumps(data, indent=1))
    resp.content_length = resp.calculate_content_length()
    return resp

api.add_resource(ClientList, '/')

if __name__ == '__main__':
    app.run(debug=True)

输出;

[
 {
  "age": null,
  "name": "Tom",
  "id": "1"
 },
 {
  "age": null,
  "email": "john@example.com",
  "name": "John",
  "id": "2"
 }
]

更新 打补丁 flask_restful.marshal
我在marshal函数内使用genexp过滤出None值,并将flask_restful.marshal替换为此处定义的marshal

from collections import OrderedDict
from flask import Flask
import flask_restful
from flask_restful import fields, marshal_with, Api, Resource

app = Flask(__name__)
api = Api(app)

class Client(object):
    def __init__(self, id, name, email=None):
        self.id = id
        self.name = name
        self.email = email

client_fields = {
    'id': fields.String,
    'name': fields.String,
    'email': fields.String,
}

def marshal(data, fields, envelope=None):
    def make(cls):
        if isinstance(cls, type):
            return cls()
        return cls

    if isinstance(data, (list, tuple)):
        return (OrderedDict([(envelope, [marshal(d, fields) for d in data])])
                if envelope else [marshal(d, fields) for d in data])

    items = ((k, marshal(data, v) if isinstance(v, dict)
              else make(v).output(k, data))
             for k, v in fields.items())
    #filtering None
    items = ((k,v) for k, v in items if v is not None)
    return OrderedDict([(envelope, OrderedDict(items))]) if envelope else OrderedDict(items)

flask_restful.marshal = marshal

class ClientList(Resource):
    @marshal_with(client_fields)
    def get(self):
        clients =[Client(1, 'Tom'), Client(2, 'John', 'john@example.com')]
        return clients, 200

api.add_resource(ClientList, '/')

if __name__ == '__main__':
    app.run(debug=True)

输出;

[   
    {   
        "id": "1",
        "name": "Tom"
    },
    {   
        "email": "john@example.com",
        "id": "2",
        "name": "John"
    }
]

6
嘿,投票反对者,请留下评论,这样大家才能从错误中学习。 - Nizam Mohamed

3

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接