Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Passing array in query via axios #517

Open
LaundroMat opened this issue May 19, 2020 · 6 comments
Open

Passing array in query via axios #517

LaundroMat opened this issue May 19, 2020 · 6 comments
Labels

Comments

@LaundroMat
Copy link

LaundroMat commented May 19, 2020

Hi,

I'm using axios to send a GET request to my flask-smorest endpoint.
I know there's no real agreement on this spec-wise, but the request's querystring contains an array, and axios sends it in this format:

http://127.0.0.1:5000/api/v1/items/?types[]=cd&types[]=dvd

I've defined the schema used for reading the arguments as

class FilterSchema(ma.Schema):
    ...
    types = ma.fields.List(ma.fields.String(), missing=[])

Yet when I try to read the data received in my endpoint, types is empty:

@items_blp.route('/')
class ItemCollection(MethodView):
    @items_blp.arguments(FilterSchema(unknown=ma.EXCLUDE), location="query")
    @listings_blp.response(ItemSchema(many=True))
    def get(self, payload):
        # payload['types'] is empty...
        if payload['types']:
            qs = Item.objects.filter(item_type__in=payload['types'])
        return qs

Is this a missing feature in flask-smorest or should I ask on SO whether I should use another way to pass data?

@lafrech
Copy link
Member

lafrech commented May 19, 2020

There's two issues

  • 1/ Parsing this query string format correctly.
  • 2/ Documenting it correctly

1/ is a webargs issue, so I'll transfer to webargs. You'll have to write a custom parser (see https://webargs.readthedocs.io/en/latest/advanced.html#custom-parsers).

2/ is an apispec issue. I'm not sure there's a way to write a spec that will display correctly in a frontend such as ReDoc or Swagger-UI, so you might end up just adding a generic comment at the beginning of the spec.

@lafrech lafrech transferred this issue from marshmallow-code/flask-smorest May 19, 2020
@LaundroMat
Copy link
Author

LaundroMat commented May 19, 2020

Great, thanks for pointing me in the right direction!

Update: I just realized I probably should leave this ticket open so that the webargs team can have a look at it and decide to take it up or not.

@sirosen
Copy link
Collaborator

sirosen commented May 22, 2020

As @lafrech said, you could write a custom parser which handles this for you. That's the best approach available today.

Since axios appears to be a pretty popular, I'd be happy to have a new section in the docs for examples, with an AxiosQueryParser example, or something similar.

It looks to me like axios is actually using another library, qs, for the encoding? Do we know how widespread usage of that tool is?
I'm not sure if we should be thinking about direct support within webargs for this querystring format yet, but I'll sleep on the idea, at least.

Looking at how qs encodes arrays and objects (link: https://www.npmjs.com/package/qs ), I think you can get something pretty good quite similar to how the current example parser is done.

@LaundroMat
Copy link
Author

Thank you for considering this.

FWIW, I cobbled something together that serves my purpose, but it's far from a full parser of qs generated querystrings. It doesn't handle complex nested objects for example (see the last test in the code below).

I might try and create a PR later, but I wanted to leave this already here if that's ok with you.

By the way, I couldn't find a way for marshmallow or flask_smorest to use this parser. If someone can point me in the right direction, I'd like to add an explanation to the docs about that.

import re
from webargs.flaskparser import FlaskParser
from werkzeug.datastructures import ImmutableMultiDict

class AxiosQueryFlaskParser(FlaskParser):
    """
    WIP: Parse axios generated query args
    
    This parser handles query args generated by the Javascript qs library's stringify methods as explained at https://www.npmjs.com/package/qs#stringifying

    For example, the URL query params `?names[]=John&names[]=Eric`
    will yield the following dict:

        {
            'names': ['John', 'Eric']
        }
    """

    def load_querystring(self, req, schema):
        return _structure_dict(req.args)

def _structure_dict(dict_):
  # dict is req.args, a ImmutableMultiDict that _can_ contain double entries
  # e.g. ([('filters[]', 'size'), ('filters[]', 'color'), ('sortBy', 'price')
  return_value = {}
  for k,v in dict_.items():
    m = re.match(r"(\w+)\[(.*)\]", k)

    if m:
      print("groups: ", m.groups())
      if m.group(2) == '':
        # empty [], i.e. a list of values k
        # eg a[]=b, a[]=c
        return_value[k[:-2]] = dict_.getlist(k)
      else:
        try:
          int(m.group(2))
          if return_value.get(m.group(1)):
            # there is already an item with this key in the return_value, so add it
            return_value[m.group(1)].append(v)
          else:
            return_value = {m.group(1): [v]}
        except ValueError:  # not an int
          # eg a[b]=c
          return_value[m.group(1)] = {m.group(2): v}
    else:
      # no [] in args
      if "," in v:
          # eg a=b,c
          return_value[k] = v.split(',')
      else:
        if len(dict_.getlist(k)) > 1:
          # eg a=b, a=c
          return_value[k] = dict_.getlist(k)
        else:
          # eg a=b
          return_value[k] = v
  return return_value

def test_extract_args_from_qs(args):
  return _structure_dict(args)

# tests based on https://www.npmjs.com/package/qs#stringifying

# simple
args = ImmutableMultiDict([('a', 'b')])
assert test_extract_args_from_qs(args) == {'a': 'b'}

# array
args = ImmutableMultiDict([('a[]', 'b'), ('a[]', 'c')])
assert test_extract_args_from_qs(args) == {'a': ['b', 'c']}

# object
args = ImmutableMultiDict([('a[b]', 'c')])
assert test_extract_args_from_qs(args) == {'a': {'b': 'c'}}

# different array formats
args = ImmutableMultiDict([('a[0]', 'b'), ('a[1]', 'c')])
assert test_extract_args_from_qs(args) == {'a': ['b', 'c']}

args = ImmutableMultiDict([('a[]', 'b'), ('a[]', 'c')])
assert test_extract_args_from_qs(args) == {'a': ['b', 'c']}

args = ImmutableMultiDict([('a', 'b'), ('a', 'c')])
assert test_extract_args_from_qs(args) == {'a': ['b', 'c']}

args = ImmutableMultiDict([('a', 'b,c')])
assert test_extract_args_from_qs(args) == {'a': ['b', 'c']}

args = ImmutableMultiDict([('a', 'b'), ('c[0]', 'd'), ('c[1]', 'e=f'),('f[0][0]', 'g'), ('f[1][0]', 'h')])
assert test_extract_args_from_qs(args) == {'a': 'b', 'c': ['d', 'e=f'], 'f':[['g'],['h']]}


@ThiefMaster
Copy link
Contributor

ThiefMaster commented Nov 18, 2020

Just FYI, you can configure axios to not do this [] nonsense that started in PHP when they thought it'd be even remotely a good idea to let the client decide whether unstructured form data should become an array or not...

Just pass paramsSerializer: params => qs.stringify(params, {arrayFormat: 'repeat'}) to your axios calls (or better: use axios.create() to create an instance where you pass this as a setting and use that instance everywhere)

@BorjaEst
Copy link

To support both communities (agree axios looks wired... ejm ejm) the better would might be to "decode" the axios transformation back.

To do so, you mentioned that you are usingmarshmallow&flask_smorest, so my best approach would be to use schemas and apre_load:

from marshmallow import Schema, pre_load
from werkzeug.datastructures import ImmutableMultiDict
...

class BaseSchema(Schema):
    """Base schema to control your common schema features."""
    class Meta:
        """Normally your Meta go here"""        
        ....

    @pre_load   # Support PHP¿? and axios query framework
    def process_input(self, data, **kwargs):
        fixed_args = [(x.replace('[]', ''), y) for x,y in data.data._iter_hashitems()]
        data.data = ImmutableMultiDict(fixed_args)
        return data

Note it is apre_loadin all your schemas, it adds a bit of overhear to all your request lineal to the amount of parameters in the query that is why I put it into a list comprehension. Flexibility comes at a cost, then it is up to you to decide.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants