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

Support multiple responses with different response codes and schemes #327

Closed
victorcrimea opened this issue Jul 12, 2022 · 6 comments · Fixed by #458
Closed

Support multiple responses with different response codes and schemes #327

victorcrimea opened this issue Jul 12, 2022 · 6 comments · Fixed by #458
Assignees
Milestone

Comments

@victorcrimea
Copy link

Now @app.doc decorator now accepts responses in a form of a dict {status_code: description} which is not enough to describe an endpoint wthat has multiple possible status codes each one one with it's one Schema and examples.

Suggested use-case

@app.get("/user/<int:id>")
@app.doc(
    responses = {
        200: {
            "description": "Success",
            "content": {
                "application/json": {
                    "schema": UserResponseSchema
                }
            }
        },
        400: {
            "description": "Database error",
            "content": {
                "application/json": {
                    "schema": DatabaseErrorSchema,
                    "examples": {
                        "foo": {
                            "summary": "database error",
                            "value": {
                                "error": "Database error.",
                                "error_code": 1001
                            }
                            
                        }
                    }
                }
            }
        },
        401: {
            "description": "Not authorized",
            "content": {
                "application/json": {
                    "schema": NotAuthorizedErrorSchema,
                    "examples": {
                        "foo": {
                            "summary": "not authorized",
                            "value": {
                                "error": "Not authorized",
                                "error_code": 1001
                            }
                            
                        }
                    }
                }
            }
        }
    }
)
def get_user():
    # implementation goes here
    pass
@victorcrimea
Copy link
Author

Solution in PR may leads to the call chaining when decorator is being wrapped in itself several times. But type check happen early so it should be pretty shallow call chaining.

@victorcrimea victorcrimea changed the title Support multiple responses wit different response codes and schemes Support multiple responses with different response codes and schemes Jul 14, 2022
@oc-ben-ellis
Copy link

Can I currently add a view that would return either a 200 or 422 response ?

@BEllis
Copy link

BEllis commented Jun 30, 2023

I managed to implement a workaround for now.

I introduced an expect decorator, that lists any exceptions that may be thrown by the endpoint.

    @blueprint.doc(summary="Get a thingy.")
    @blueprint.get("/<thingy_id:thingy_id>")
    @blueprint.output(GetThingyResponse)
    @blueprint.expect([
        InvalidRequestError,  # raised by URL converter and request DTO validation.
        ThingyNotFoundError 
    ])
    def get_thingy(
        thingy_id: ThingyId,
    ) -> GetThingyResponseDto:
        ...

This decorator basically adds a 999 response to the responses on the _spec and it puts an dictionary instead of a string for the description. I've then got a spec_processor that searches through the generated spec for the 999 response, and adds the responses to the operation before deleting the 999 response.

One caveat was I had to wrap the doc decorator as well, as this replaces responses with None, or, if you're lazy, you can just put the doc decorator under the expect decorator so the responses isn't wiped out.

All my errors inherit from an ApiBaseException that supplies the schema, default_status_code, default_description, examples, etc.

Unfortunately I can't easily share the code as it's mixed with other changes that I've done to support attrs instead of marshmallow for generating schemas/responses/validation.

Side note: A simpler solution to adding a new expect decorator, is to use responses in the doc decorator, i.e.

@blueprint.doc(responses={
    999: [
       { "status_code": 422, schema=... },
       { "status_code": 404, schema=... },       
    ]
}

And then create a spec_processor,

def my_spec_processor(spec: dict[str, Any]) -> dict[str, Any]:
    # Find the 999 responses and get the list of objects from the "description" and modify the spec as needed. 
    ...

app.spec_processor(my_spec_processor)

@greyli greyli added this to To do in APIFlask 2.0 Jul 16, 2023
@greyli greyli added this to the 2.0.0 milestone Jul 18, 2023
@greyli greyli self-assigned this Jul 18, 2023
@greyli greyli moved this from To do to In progress in APIFlask 2.0 Jul 20, 2023
@greyli
Copy link
Member

greyli commented Jul 20, 2023

@victorcrimea @BEllis Hi guys, I'm trying to implement this in the 2.0 version. Could you please provide some complete examples of this usage? Just the example you gave me but with the main implementation inside the view function body (including all the schema definitions).

I'm currently considering support passing a complete response spec through the @app.doc(responses) decorator. Here is an example:

@app.get('/')
@app.doc(responses={
    400: {
        'description': 'Error',
        'content': {
            'application/json': {
                'schema': ErrorSchema
            }
        }
    },
    422: {
        'description': 'Another error',
        'content': {
            'application/json': {
                'schema': AnotherErrorSchema
            }
        }
    }
})
def say_hello():
    ...

Will this meet your needs?

@greyli
Copy link
Member

greyli commented Jul 20, 2023

Can I currently add a view that would return either a 200 or 422 response ?

The 422 error is added automatically when you added an app.input decorator to your view function. The spec will look like this:

... 
        "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            },
            "description": "Validation error"
        }

APIFlask 2.0 automation moved this from In progress to Done Jul 23, 2023
@victorcrimea
Copy link
Author

In this example we return either InstructorResponseGetSchema, or structured error(it has error_code, error explanation, etc).

Note that this is syntax of modified APIFlask that we forked some time ago.

class Instructor(MethodView):
    @app.input(InstructorRequestGetSchema, location='query')
    @app.output(InstructorResponseGetSchema, 200, description="Gets instructor public profile")
    @app.output(GenericErrorSchema, 450, description="Database, Key, Role, User, BotoClient errors")
    @app.doc(
        tags=['profile']
    )
    def get(self, query):
        """Get instructor"""

        instructor_data = get_instructor(query.get('instructor_id'))

        resp = serialize(InstructorResponseGetSchema, instructor_data)
        return resp, 200

I do not like how JSON-like decorator looks, but I think it may provide even more flexibility, beyond the cases we are looking at now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
No open projects
4 participants