Alex Daro
Alex Daro

Reputation: 429

Flask-restful, marshal_with + nested data

I have been stuck on this for a while now. My problem is I need to be able to use marshal_with and validate on nested fields coming from a POST. My test looks like this:

def test_user_can_apply_with_multiple_dogs(self):

    data = {
        # User data
        'registration_type': "guest",
        'first_name': 'Alex',
        'last_name': 'Daro',
        'phone_number': '805-910-9198',
        'street': '13950 NW Passage',
        'street2': '#208',
        'city': 'Marina del Rey',
        'state': 'CA',
        'zipcode': '90292',
        'photo': 'test_image.png',
        'how_did_you_hear': 0,
        #Dog data
        'pets': [
            {
                'dog_photo': "dog.png",
                'name': 'Genghis Khan',
                'breed': 'Shih Tzu',
                'age': 'Puppy',
                'size': 'Small',
            },
            {
                'dog_photo': "dog2.png",
                'name': 'Archibald',
                'breed': 'Great Dane',
                'age': 'Adult',
                'size': 'Extra Large',
            },
        ]
    }

    resp = self.client.post('/profile/registration', data=json.dumps(data))
    self.assertEqual(resp.status_code, 200)

and my endpoint class looks like this:

nested_fields = {
    'dog_photo': fields.String,
    'name': fields.String,
    'breed': fields.String,
    'age': fields.String, 
    'size': fields.String, 
}

profile_fields = {
    # 'user_email':fields.String,
    'token': fields.String,
    'registration_type': fields.String,
    'first_name': fields.String,
    'last_name': fields.String,
    'phone_number': fields.String,
    'street': fields.String,
    'street2': fields.String,
    'city': fields.String,
    'state': fields.String,
    'zipcode': fields.Integer,
    'photo': fields.String,
    'how_did_you_hear': fields.String,
    #Dog data
    'pets': fields.Nested(nested_fields)

}
class GuestProfile(Resource):
    @marshal_with(profile_fields)
    def post(self):
        # User data
        parser = reqparse.RequestParser()
        parser.add_argument('registration_type', type=str)
        parser.add_argument('first_name', type=str, required=True, help="First Name cannot be blank.")
        parser.add_argument('last_name', type=str, required=True, help="Last Name cannot be blank.")
        parser.add_argument('phone_number', type=str, required=True, help="Phone Number cannot be blank.")
        parser.add_argument('street', type=str, required=True, help="Street cannot be blank.")
        parser.add_argument('street2', type=str)
        parser.add_argument('city', type=str, required=True, help="City cannot be blank.")
        parser.add_argument('state', type=str, required=True, help="State cannot be blank.")
        parser.add_argument('zipcode', type=str, required=True, help="Zipcode cannot be blank.")
        parser.add_argument('photo', type=str, required=True, help="Please select a photo.")
        parser.add_argument('how_did_you_hear', type=str, required=True, help="How did you hear about us cannot be "
                                                                              "blank.")
        parser.add_argument('pets', type=str)
        kwargs = parser.parse_args()
        print kwargs, "KWWW" 

kwargs['pet'] always comes in as None. Anyone have any ideas?

Upvotes: 4

Views: 12338

Answers (2)

junnytony
junnytony

Reputation: 3535

You're missing 'Content-Type' header and also an action='append' for your pets argument as described here.

I tested your code above with just adding the 'Content-Type' and action as prescribed and it works in both Python 2.7 and 3.

Now the issue is it returns a list of strings since you specified your pets argument as type=str. In order to get a list of dicts, you'll have to write a custom parser type (as @Josh rightly pointed out). See example below:

Ex.

def pet_parser(pet):
    # You can do other things here too as suggested in @Josh's repsonse
    return pet

In the Resource:

parser.add_argument('pets', type=pet_parser, action='append')

and in your test function:

headers = [('Content-Type', 'application/json')]
self.client.post('url', data=data, headers=headers)

Upvotes: 1

Josh
Josh

Reputation: 1336

Here's an example in the docs of how to make a custom parser type.

Basically, you define a function that:

  • Takes the raw value of the argument pulled from the request
  • Raises ValueError if parsing or validation fails
  • Returns the argument if validation suceeds

Basic example related to your question:

def pet_list_parser(pets):
    if type(pets) != list:
        raise ValueError('Expected a list!')

    # Do your validation of the pet objects here. For example:
    for pet in pets:
        if 'name' not in pet:
            raise ValueError('Pet name is required')

        # Also do any conversion of data types here
        pet['name'] = pet['name'].capitalize()

    return pets

parser = RequestParser()
parser.add_argument('pets', type=pet_list_parser)

As you might guess, this gets klunky and annoying rather quickly. The request parsing in Flask-RESTful isn't designed to handle nested data. It works very well for querystring arguments, headers, basic JSON, etc. and can be configured to handle nested data. However if you are going to do this a lot, save yourself some pain down the road and look into a marshalling library like Marshmallow.

Upvotes: 3

Related Questions