python-restx / flask-restx

Fork of Flask-RESTPlus: Fully featured framework for fast, easy and documented API development with Flask
https://flask-restx.readthedocs.io/en/latest/
Other
2.16k stars 335 forks source link

Choices Property Does Not Work For JSON List #535

Open pype-leila opened 1 year ago

pype-leila commented 1 year ago

When defining choices for a list in the JSON data input, validation does not work. This is true if type is list or if type is str and action is "append".

Code

import flask
from flask_restx import Api, Namespace, Resource
from flask_restx import reqparse
from flask_restx import Api

parser = reqparse.RequestParser()
parser.add_argument(
    "argList",
    dest="arg_list",
    type=list,
    location="json",
    default=[],
    choices=[
        "one",
        "two",
        "three",
    ],
    required=False,
    help=f"An argument list",
)

# Our Flask app and API
app = flask.Flask(__name__)
api = Api(
    app,
    version="1.0.0",
    title="Tester",
    description="Test parsing arguments",
)

class RouteWithArgs(Resource):

    @api.expect(parser)
    def put(
        self,
    ):
        args = parser.parse_args()
        return {"data": "Args look good!"}, 200

# routes
api.add_resource(RouteWithArgs, "/args")

if __name__ == "__main__":

    app.run(debug=True)

Repro Steps (if applicable)

  1. Run Flask application for code above with python <file-name>.py
  2. Send a request with either allowed or disallowed values
  3. Observe that you receive an error message either way

Expected Behavior

I would expect to receive an error message with a disallowed parameter and no error message when providing allowed parameters.

Actual Behavior

An error is returned no matter what is present in the request.

Error Messages/Stack Trace

>>> response.json()
{'errors': {'argList': "An argument list The value '['a']' is not a valid choice for 'argList'."}, 'message': 'Input payload validation failed'}
>>> response = requests.put("http://localhost:5000/args", headers={"Content-Type": "application/json"}, data=json.dumps({"argList": ["one"]}))
>>> response.json()
{'errors': {'argList': "An argument list The value '['one']' is not a valid choice for 'argList'."}, 'message': 'Input payload validation failed'} 

Environment

peter-doggart commented 1 year ago

In flask-restx choices is dumb in the sense it only compares like for like exactly. In your instance, it doesn't work because your choices aren't valid lists, which is the data type you request.

If the lists you expect are known in advance (in the correct order), then you could do something like:

parser = reqparse.RequestParser()
parser.add_argument(
    "argList",
    dest="arg_list",
    type=list,
    location="json",
    default=[],
    choices=[
        ["one"],
        ["two"],
        ["three"],
        ["one", "two"],
    ],
    required=False,
    help=f"An argument list",
)

In that case, those exact lists will be matched and validated.

However, what I expect you actually want is to be able to pass a list that contains any number of valid choices in any order. This is possible, but you need to write your own custom validation function. You can do this for any input but providing a function to the type field of add_argument() which provides one of two options: a) The valid value or b) Raises a ValueError.

In your example, you can do something like this to get a valid list:

ALLOWED_VALUES = ["one", "two", "three"]
def list_validator(x):
    if type(x) != list:
        raise ValueError(f"'{x}' was not a list.")
    if not all(value in ALLOWED_VALUES for value in x):
        raise ValueError(f"'{x}' contains one or more invalid choices.")
    return x

parser = reqparse.RequestParser()
parser.add_argument(
    "argList",
    dest="arg_list",
    type=list_validator,
    location="json",
    required=False,
    help=f"An argument list"
)

This will then give you the expected behaviour:

curl -X POST -d '{"argList": "one"}' -H "Content-Type: application/json" http://127.0.0.1:5000/args
{
    "errors": {
        "argList": "An argument list 'one' was not a list."
    },
    "message": "Input payload validation failed"
}

curl -X POST -d '{"argList": ["one"]}' -H "Content-Type: application/json" http://127.0.0.1:5000/args
{
    "data": [
        "Args look good!"
    ]
}

curl -X POST -d '{"argList": ["one", "two"]}' -H "Content-Type: application/json" http://127.0.0.1:5000/args
{
    "data": [
        "Args look good!"
    ]
}

curl -X POST -d '{"argList": ["one", "two", "four"]}' -H "Content-Type: application/json" http://127.0.0.1:5000/args
{
    "errors": {
        "argList": "An argument list '['one', 'two', 'four']' contains one or more invalid choices."
    },
    "message": "Input payload validation failed"
}
pype-leila commented 1 year ago

To me, the intuitive behavior -- and what I want to accomplish -- would be to determine whether all items in the provided input are both unique and elements of choices, ie the input list is a subset of choices. I have used a custom validator in my own code to accomplish this, but this still seems like a bug to me. At the very least, there should be explicit documentation of the way it does work.