Search code examples
pythonflaskmodelflask-restplus

flask-restplus fields.Nested() with raw Dict (not model)


Spoiler alert: I posted my solution as an answer to this question

I am using flastk-resptlus to create an API. I have to provide the data in a specific structure, which I have problems to get, see an example below:

What I need to get is this structure:

{
    "metadata": {
        "files": [] 
    },
    "result" : {
        "data": [
                {
                 "user_id": 1,
                  "user_name": "user_1",
                  "user_role": "editor"
                },
                {
                  "user_id": 2
                  "user_name": "user_2",
                  "user_role": "editor"
                },
                {
                  "user_id": 3,
                  "user_name": "user_3",
                  "user_role": "curator"
                }
            ]
    }
}

But the problem comes that I cannot manage to get the structure of "result" : { "data": []} without making "data" a model itself.

What I tried to do so far (and did not work)

# define metadata model
metadata_model = api.model('MetadataModel', {
          "files": fields.List(fields.String(required=False, description='')),
}
# define user model 
user_model = api.model('UserModel', {
          "user_id": fields.Integer(required=True, description=''),
          "user_name": fields.String(required=True, description=''),
          "user_role": fields.String(required=False, description='')
}

# here is where I have the problems
user_list_response =  api.model('ListUserResponse', {
            'metadata': fields.Nested(metadata_model),
            'result' :  {"data" : fields.List(fields.Nested(user_model))}
             })

Complains that cannot get the "schema" from "data" (because is not a defined model), but I don't want to be a new api model, just want to append a key called "data". Any suggestions?

This I tried and works, but is not what I want (because I miss the "data"):

user_list_response =  api.model('ListUserResponse', {
            'metadata': fields.Nested(metadata_model),
            'result' :  fields.List(fields.Nested(user_model))
            })

I don't want data to be a model because the common structure of the api is the following:

{
    "metadata": {
        "files": [] 
    },
    "result" : {
        "data": [
                <list of objects> # here must be listed the single model
            ]
    }
}

Then, <list of objects> can be users, addresses, jobs, whatever.. so I want to make a "general structure" in which then I can just inject the particular models (UserModel, AddressModel, JobModel, etc) without creating a special data model for each one.


Solution

  • My workaround solution that solves all my problems:

    I create a new List fields class (it is mainly copied from fields.List), and then I just tune the output format and the schema in order to get the 'data' as key:

    class ListData(fields.Raw):
        '''
        Field for marshalling lists of other fields.
    
        See :ref:`list-field` for more information.
    
        :param cls_or_instance: The field type the list will contain.
    
        This is a modified version of fields.List Class in order to get 'data' as key envelope
        '''
        def __init__(self, cls_or_instance, **kwargs):
            self.min_items = kwargs.pop('min_items', None)
            self.max_items = kwargs.pop('max_items', None)
            self.unique = kwargs.pop('unique', None)
            super(ListData, self).__init__(**kwargs)
            error_msg = 'The type of the list elements must be a subclass of fields.Raw'
            if isinstance(cls_or_instance, type):
                if not issubclass(cls_or_instance, fields.Raw):
                    raise MarshallingError(error_msg)
                self.container = cls_or_instance()
            else:
                if not isinstance(cls_or_instance, fields.Raw):
                    raise MarshallingError(error_msg)
                self.container = cls_or_instance
        def format(self, value):
    
            if isinstance(value, set):
                value = list(value)
    
            is_nested = isinstance(self.container, fields.Nested) or type(self.container) is fields.Raw
    
            def is_attr(val):
                return self.container.attribute and hasattr(val, self.container.attribute)
    
            # Put 'data' as key before the list, and return the dict
            return {'data': [
                self.container.output(idx,
                    val if (isinstance(val, dict) or is_attr(val)) and not is_nested else value)
                for idx, val in enumerate(value)
            ]}
    
        def output(self, key, data, ordered=False, **kwargs):
            value = fields.get_value(key if self.attribute is None else self.attribute, data)
            if fields.is_indexable_but_not_string(value) and not isinstance(value, dict):
                return self.format(value)
    
            if value is None:
                return self._v('default')
            return [marshal(value, self.container.nested)]
    
        def schema(self):
            schema = super(ListData, self).schema()
            schema.update(minItems=self._v('min_items'),
                          maxItems=self._v('max_items'),
                          uniqueItems=self._v('unique'))
    
            # work around to get the documentation as I want
            schema['type'] = 'object'
            schema['properties'] = {}
            schema['properties']['data'] = {}
            schema['properties']['data']['type'] = 'array'
            schema['properties']['data']['items'] = self.container.__schema__
    
            return schema