Skip to content

Passing array in query via axios #517

Closed

Description

@LaundroMat

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?

Activity

  1. lafrech commented on May 19, 2020

    @lafrech
    Member

    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.

  2. LaundroMat commented on May 19, 2020

    @LaundroMat
    Author

    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.

  3. sirosen commented on May 22, 2020

    @sirosen
    Collaborator

    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.

  4. LaundroMat commented on May 26, 2020

    @LaundroMat
    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']]}
    
    
    
  5. ThiefMaster commented on Nov 18, 2020

    @ThiefMaster
    Contributor

    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)

  6. BorjaEst commented on Aug 23, 2021

    @BorjaEst

    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.

  7. sloria commented on Jun 8, 2026

    @sloria
    Member

    Closing for now since this is quite old and the solution for this is outside the scope of webargs itself. Feel free to comment if further discussion is needed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions