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

Скажем, у меня есть таблица клиентов с полями id, имя и электронная почта. Поле электронная почта необязательно.

Код выглядит следующим образом:

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"
}

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

Ответы

Ответ 1

Я бы использовал 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}

Ответ 2

Есть два способа сделать это: предварительная сортировка и постмаршаллинговая модификация. Предварительная сортировка удаляет значения по умолчанию, заданные именам полей в client_fields dict, но post-marshalling сохраняет их.

В методе предварительного сортировки вам необходимо передать измененные поля 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"
 }
]

обновление patch 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"
    }
]