Return empty JSON object with Flask-Restful Nested field object for SQLAlchemy association if association is None

4.2k Views Asked by At

The summary might be very confusing but I don't know how to formulate it more concise.

The models I have:

class Movie(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(
    imdb_data = db.relationship('IMDBData', uselist=False)

class IMDBData(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(255))
    rating = db.Column(db.Float)
    movie_id = db.Column(db.Integer, db.ForeignKey('movie.id'))

Using Flask-Restful fields I am marshaling the response like this:

imdb_data_fields = {
    'id': fields.Integer,
    'title': fields.String,
    'rating': fields.Float
}

movie_fields = {
    'id': fields.Integer,
    'title': fields.String
}

class MovieListAPI(Resource):
    def __init__(self):
        self.parser = reqparse.RequestParser()
        super(MovieListAPI, self).__init__()

    def get(self):
        self.parser.add_argument('imdb_data', type=str, location='args')
        args = self.parser.parse_args()

        m_fields = copy.copy(movie_fields)

        # TODO: Return empty object if movie.imdb_data = None
        if args['imdb_data']:
            m_fields['imdb_data'] = fields.Nested(imdb_data_fields)

        movies = Movie.query.all()
        return {'movies': marshal(movies, m_fields)}

Now if it happens that a movie does not have a corresponding imdb_data record, i.e. Movie.query.filter_by(id=123).first().imdb_data = None that movie's object is marshaled like this:

{
    "id": 1302, 
    "imdb_data": {
        "id": 0, 
        "rating": null, 
        "title": null
    },     
    "title": "F 63 9 Love Sickness"
}

Instead I want the response to look like this:

{
    "id": 1302, 
    "imdb_data": {},     
    "title": "F 63 9 Love Sickness"
}

I know how to hack this when I return one movie (by id):

if args['imdb_data']:
    if movie.imdb_data:
        m_fields['imdb_data'] = fields.Nested(imdb_data_fields)
    else:
        m_fields['imdb_data'] = fields.Nested({})

But how do I do that for the list of movies? Probably I could go through the array myself and change it by hand but there must be a more efficient way.

3

There are 3 best solutions below

0
On BEST ANSWER

This can be achieved by creating a custom field, like this:

class NestedWithEmpty(Nested):
    """
    Allows returning an empty dictionary if marshaled value is None
    """
    def __init__(self, nested, allow_empty=False, **kwargs):
        self.allow_empty = allow_empty
        super(NestedWithEmpty, self).__init__(nested, **kwargs)

    def output(self, key, obj):
        value = get_value(key if self.attribute is None else self.attribute, obj)
        if value is None:
            if self.allow_null:
                return None
            elif self.allow_empty:
                return {}

        return marshal(value, self.nested)

and then using it to marshal objects passing allow_empty=True:

m_fields['imdb_data'] = NestedWithEmpty(imdb_data_fields, allow_empty=True)

I even created a pull request with this feature: https://github.com/twilio/flask-restful/pull/328

0
On

After reading the PR #328 (thanks @Andriy), and following it, my fix was to add the default arg

foo['bar'] = fields.Nested(nested_fields, default={})

Wasn't obvious in the docs.

0
On

Starting from 0.11.0, you can use the option skip_none=True to return an empty object instead of nulls.

Example with @marshal_with:

from flask_restplus import Model, fields, marshal_with
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}

Specifying on nested field:

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

source: https://flask-restplus.readthedocs.io/en/0.11.0/marshalling.html#skip-fields-which-value-is-none