Flask-RESTful - не возвращать свойство объекта вместо возврата null

Допустим, у меня есть таблица клиентов с полями id, name и 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"
}

Что в основном означает, что вместо свойства с нулевым значением я хочу, чтобы оно не возвращало вообще никакого такого свойства. Есть ли способ добиться этого?


person zorza    schedule 20.03.2015    source источник
comment
Как правило, это не самое удачное дизайнерское решение. Любые потребители вашего API, которые ищут поле электронной почты, теперь будут нуждаться в специальной обработке, чтобы иметь дело с тем, будет ли это поле там или нет. Для ваших потребителей намного безопаснее иметь нулевое поле, а не поле, которое может быть или не быть.   -  person kylieCatt    schedule 15.03.2016
comment
Я не совсем согласен с этим... объекты могут быть из нескольких источников данных и иметь множество неполных данных. В каждом конкретном случае иногда это вполне законное решение.   -  person Magoo    schedule 24.11.2020


Ответы (2)



Это можно сделать двумя способами: предварительная и постсортировочная модификация. Предварительная сортировка удаляет все значения по умолчанию, присвоенные именам полей в client_fields dict, но после сортировки сохраняет их.

В методе предварительной сортировки вам необходимо передать измененные поля с dict по marshal, если адрес электронной почты клиента None.
Например;

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', '[email protected]')]
    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": "[email protected]", "id": "2", "name": "John"}]

При постсортировке вы должны удалить поле электронной почты OrderedDict, возвращенное marshal_with, если оно None.
Функция de_none по умолчанию удаляет все поля, которые являются None, или вы должны явно передать имена полей, если это нежелательно и вы также должны передать аргумент envelope, если marshal_with принимает то же самое.

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='[email protected]')
    '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', '[email protected]')], 200, {'Etag': 'blah'}
    #return [Client(1, 'Tom'), Client(2, 'john', '[email protected]')]
    #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', '[email protected]'), 201, {'Etag': 'ok'}

print(json.dumps(get()))

Выход;

{"id": "1", "name": "Tom"}
{"email": "[email protected]", "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', '[email protected]')]
        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": "[email protected]",
  "name": "John",
  "id": "2"
 }
]

обновление исправление flask_restful.marshal
Я отфильтровываю значения None в genexp внутри функции marshal и заменяю 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', '[email protected]')]
        return clients, 200

api.add_resource(ClientList, '/')

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

Выход;

[   
    {   
        "id": "1",
        "name": "Tom"
    },
    {   
        "email": "[email protected]",
        "id": "2",
        "name": "John"
    }
]
person Nizam Mohamed    schedule 21.03.2016
comment
эй, проголосовавший против, пожалуйста, оставьте комментарий, чтобы люди могли учиться на ошибках. - person Nizam Mohamed; 23.03.2016