marshmallow-code / flask-smorest

DB agnostic framework to build auto-documented REST APIs with Flask and marshmallow
https://flask-smorest.readthedocs.io
MIT License
634 stars 72 forks source link

Endpoint isn't getting data from json_or_form arguments when using the Swagger UI #653

Closed cjproud closed 1 month ago

cjproud commented 1 month ago

Hi there,

I have a route that generates a JWT and it should expect either form data or JSON data. If I supply the arguments decorator as either of these:

@blp.arguments(AuthJWTLoginArgsSchema, location="json") @blp.arguments(AuthJWTLoginArgsSchema, location="form")

I can correctly get the username and password within the args of the post method. However, if I apply the decorator like so:

@blp.arguments(AuthJWTLoginArgsSchema, location="json_or_form")

the interface looks like below and the data isn't available with the request when I make the request from the UI. Note that if I manually curl that endpoint with either form or json payloads it works so it seems like it's a Swagger UI issue maybe?

Screenshot 2024-05-31 at 5 31 01 pm

cjproud commented 1 month ago

Saying that, an easy solution would be to override the parameter fields above to supply a "json payload style UI" like below instead, is that possible?

Screenshot 2024-05-31 at 5 50 25 pm

lafrech commented 1 month ago

I guess the issue here is that json_or_form is a webargs shortcut that is unknown to apispec and Swagger UI.

We'd need to figure out how to specify in OpenAPI that an argument is expected in several locations and add code somewhere (flask-smorest, as apispec does not include webargs specifics) to turn json_or_form into this.

But I'm afraid apispec is not ready for this (multiple locations).

This was discussed in https://github.com/marshmallow-code/apispec/issues/548 although the conversation quickly moved off-topic.

cjproud commented 1 month ago

No problem, what I'm thinking is that I'll make the route a regular json location decorated route and have some middleware to convert the form data to json data. Do you know if that's possible?

Otherwise, could I have some form of custom parser for a json decorated route that uses the webargs json_or_form parser instead?

lafrech commented 1 month ago

Not sure I'm following you. I think what you're doing works, it just isn't documented properly in OpenAPI. I'm not sure this is achievable, at least in a reasonable way.

You can keep your json_or_form code and live with an incomplete doc, perhaps even tweak __location_map__ in apispec to document it as json only so as to make SwaggerUI believe it is json so that the interactive doc works (json only, better than nothing).

cjproud commented 1 month ago

Not sure I'm following you. I think what you're doing works, it just isn't documented properly in OpenAPI. I'm not sure this is achievable, at least in a reasonable way.

You can keep your json_or_form code and live with an incomplete doc, perhaps even tweak __location_map__ in apispec to document it as json only so as to make SwaggerUI believe it is json so that the interactive doc works (json only, better than nothing).

Yep that's exactly what I've done, I've added a custom parser on a new Blueprint instance that changes json_or_form to load_json:

from webargs.flaskparser import FlaskParser
from flask_smorest import Blueprint
class MyFlaskParser(FlaskParser):
    __location_map__: dict[str, str | typing.Callable] = {
    "json": "load_json",
    "querystring": "load_querystring",
    "query": "load_querystring",
    "form": "load_form",
    "headers": "load_headers",
    "cookies": "load_cookies",
    "files": "load_files",
    "json_or_form": "load_json",
}
class MyBlueprint(Blueprint):
    ARGUMENTS_PARSER = MyFlaskParser()

blp2 = MyBlueprint(
    "authentication", __name__, url_prefix="/auth", description="Authentication"
)
lafrech commented 1 month ago

IIUC, your code does the opposite: it modifies the mapping in webargs, so it document as json_or_form (which won't work) and parse as json (which excludes form). I meant modify the mapping in apispec to document as json only but keep the parsing of both json and form in webargs unchanged.

(But if I misunderstood and you're happy with your changes, then that's fine.)

cjproud commented 1 month ago

IIUC, your code does the opposite: it modifies the mapping in webargs, so it document as json_or_form (which won't work) and parse as json (which excludes form). I meant modify the mapping in apispec to document as json only but keep the parsing of both json and form in webargs unchanged.

(But if I misunderstood and you're happy with your changes, then that's fine.)

That would be this mapping correct?


    __location_map__: dict[str, str | typing.Callable] = {
        "json": "load_json_or_form",
        "querystring": "load_querystring",
        "query": "load_querystring",
        "form": "load_form",
        "headers": "load_headers",
        "cookies": "load_cookies",
        "files": "load_files",
        "json_or_form": "load_json_or_form",
    }

That's what I actually did haha, apologies I made a mistake in my last code snippet :)

lafrech commented 1 month ago

I meant modify the mapping in apispec here to add "json_or_form": "body", so that json_or_form works but is documented as json/body.