tfranzel / drf-spectacular

Sane and flexible OpenAPI 3 schema generation for Django REST framework.
https://drf-spectacular.readthedocs.io
BSD 3-Clause "New" or "Revised" License
2.38k stars 264 forks source link

I can not describe my custom response adequat. #760

Closed Wissperwind closed 2 years ago

Wissperwind commented 2 years ago

I have a custom view. It does not respond something created by a django serializer. At the moment I described it like this:

responses={
            (200, 'application/json'):
                inline_serializer(
                    "Result",
                    fields={
                        "results": serializers.JSONField(),
                        "errors": serializers.JSONField()
                    },
                ),
        },

This results in:

{
  "results": {
    "additionalProp1": "string",
    "additionalProp2": "string",
    "additionalProp3": "string"
  },
  "errors": {
    "additionalProp1": "string",
    "additionalProp2": "string",
    "additionalProp3": "string"
  }
}

But this is not how my endpoint works. You get either a list of errors or a dict of results. But in both cases I thought the code 200 would be correct, because the server did it's thing. The following problems:

Wissperwind commented 2 years ago

Is it possible to type in directly the text that is displayed in the results field? I think it would be mouch easier than writing serializers. Also I miss the option to specify the content of my result:

"results": {
    "additionalProp1": "string",
    "additionalProp2": "string",
    "additionalProp3": "string"
  }

Ok, the result is some kind of dict. But I would like to tell the user that the keys in that result dict are "pdf", "xml", and "csv".

tfranzel commented 2 years ago

This results in:

This does not look like something spectacular generates

(200, 'application/json'): is a key in a dict. So I can not descibe different 200 answers.

You can't by design. You can however use PolymorphicProxySerializer, which lets you model OR requests/responses.

The errors parameter in reality is a list of strings. So ["error 1", "error 2"] How can I specify this?

There is likely a way to model this with PolymorphicProxySerializer and inline_serializer but you are kind of doing something unusual which might make it cumbersome. The easiest way to do would be by raw schema. This is usually a last resort but might be warranted here

    from drf_spectacular.plumbing import build_array_type, build_basic_type
    from drf_spectacular.types import OpenApiTypes

    @extend_schema(
        responses={
            (200, 'application/json'): {
                'oneOf': [
                    build_array_type(build_basic_type(str)),
                    build_basic_type(OpenApiTypes.OBJECT)  # or build_basic_type(dict)
                ]
            }
        },
    )

How can I add explanative text about the response of the view?

@extend_schema(summary="foo", description="bar" responses=...)

Wissperwind commented 2 years ago

This would be nice. So the endpoint does it's thing, and it gives a 200 because the request was correct and so on. But there are two cases. The case where it worked, and the case where something went wrong.

responses={
            (200, 'application/json'): {
                'oneOf': [
                    inline_serializer(
                        "Result",
                        fields={
                            "results": JSONField()
                        },
                    ),
                    inline_serializer(
                        "Error",
                        fields={
                            "errors": GeneralErrorSerializer(),
                        }
                    )

                ]
            }
        },
Wissperwind commented 2 years ago

What makes you think the "this results in" part in the first post was nothing generated by drf-spectacular? Of course it was. It was directly copied from the response section of the swagger page.

tfranzel commented 2 years ago

What makes you think the "this results in" part in the first post was nothing generated by drf-spectacular?

nevermind. you showed the example output of swagger-ui, which was not immediately clear to me. In the schema it looks a bit different: "additionalProperties": {},

Regarding your example. That does not work as you either do a raw schema or let spectacular process serializers. Can't do both. However, you you could use PolymorphicProxySerializer instead of the oneOf dict which is again valid.

        PolymorphicProxySerializer(
            component_name='SomeName',
            serializers=[inline_serializer(...), inline_serializer(...)],
            resource_type_field_name=None,
        )

Apart from that, the ErrorSerializer would be wrong as it returns a object which is not what I understood what you need.

your version: {"errors": ["error1", "error1"]} my version: ["error1", "error1"]

Also consider dropping (200, 'application/json'): for 200: as it might already be discovered correctly.

Wissperwind commented 2 years ago

Ok. I tried:

responses={
            200:
                PolymorphicProxySerializer(
                    component_name='SomeName',
                    serializers=[
                        inline_serializer(
                            "Result",
                            fields={
                                "results": JSONField()
                            },
                        ),
                        inline_serializer(
                            "Error",
                            fields={
                                "errors": GeneralErrorSerializer(),
                            },
                        ),
                    ],
                    resource_type_field_name=None,
                )
        },

In Swagger that just looks like:

{
  "results": {
    "additionalProp1": "string",
    "additionalProp2": "string",
    "additionalProp3": "string"
  }
}

Maybe I am on the wrong path. What is the best practise to build and document an enpoint that could give some errors or some results?

tfranzel commented 2 years ago

so you are misleading yourself. swagger-ui will only show the example for the first oneOf item. that is a design choice by swagger-ui. If you want to understand whats going on I urge to to also look into the schema as a sanity check. swagger-ui sometimes makes unexpected choices. It will however show up in the Schema tab.

image

image

If you want to make those examples more prominent, you will need to add explicit manual examples with OpenApiExample. Then there will be a dropdown directly visible in the "value" tab.

tfranzel commented 2 years ago

for error handling it is more RESTy if you do

@extend_schema(responses={
  200: RegularSerializer,
  400: ErrorSerializer,
  "4XX": GenericErrorSerializer,
})

it is bad practice to give 200 on errors. imho that is why you struggle to model this properly. OpenAPI will make it difficult if you go against the grain.

Wissperwind commented 2 years ago

Ah, it is recommended to respond with 400 Bad Request, even if the request syntax was correct but for example some data is missing. I will do that. Thanks! I am now at that state: Unbenannt

I would wish for something like that:

sol

Wissperwind commented 2 years ago

Of course withou the red lines. That was just my german powerpoint.

tfranzel commented 2 years ago

2 choices.

  1. do a nested inline_serializer to flesh out xml etc. inline_serializer(..., fields={results: inline_serializer(...)})
  2. add an explicit example:
    @extend_schema(
        responses={
            200: RegularSerializer,
            400: ErrorSerializer,
            "4XX": GenericErrorSerializer,
        },
        examples=[
            OpenApiExample(
                'Example name',
                summary="some text",
                value={'xml': 'link to xml file', 'pdf': 'link to pdf file', },
                # response_only=True, # only needed for POST
                # status_codes=[..]  # not needed for 200,201
            )
        ]
    )

    there are many ways to provide examples. this is just one way. can also be attached to serializer or wrapped in responses with OpenApiResponse()

Wissperwind commented 2 years ago

Ah, I have not noticed on read the docs that there is a "general" example section, that makes examples for the response. I was just focused on the examples for the parameter. I think I can describe my results now. Thanks!