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

Add requestBody component #886

Open
Kostadin-Ivanov opened this issue Feb 15, 2024 · 14 comments
Open

Add requestBody component #886

Kostadin-Ivanov opened this issue Feb 15, 2024 · 14 comments

Comments

@Kostadin-Ivanov
Copy link

Hello there.

I am trying to create a path operation using ApiSpec as follows:
apiSpec.path(
path = "/my-item",
operations = dict(
post = dict(
tags = ["MyItem"],
operationId = "createMyItem",
description = "Create MyItem",
requestBody ={ "$ref": "#/components/requestBodies/MyItem" } ,
responses = {'200': {'$ref': '#/components/responses/OK'}, '500': {'$ref': '#/components/responses/GeneralError'}}
)
))

When I try to pass the ResponseBody as ref, as above, I get below error:

File "c:\workspace.venv\Lib\site-packages\apispec\core.py", line 549, in path
self.components.resolve_refs_in_path(self._paths[path])
File "c:\workspace.venv\Lib\site-packages\apispec\core.py", line 410, in resolve_refs_in_path
self._resolve_refs_in_operation(path[method])
File "c:\workspace.venv\Lib\site-packages\apispec\core.py", line 382, in _resolve_refs_in_operation
self._resolve_refs_in_request_body(operation["requestBody"])
File "c:\workspace.venv\Lib\site-packages\apispec\core.py", line 352, in _resolve_refs_in_request_body
for media_type in request_body["content"].values():
~~~~~~~~~~~~^^^^^^^^^^^
KeyError: 'content'

I could pass the whole ResponseBody schema, which is already present in my apiSpec.options['components']['requestBodies']['MyItem'], but this would add duplication for each of my path operations and I would like to avoid this.

Could you advice if it is possible to pass a $ref for RequestBody, which should result in:

post:
description: Create MyItem
operationId: createMyItem
requestBody:
$ref: '#/components/requestBodies/MyItem'

In my generated .yaml file?

Also some doc with more real case example would be very handy to work with the ApiSpec library. I checked examples in the ApiSpec read the docs but I could not find proper example of how to add / manipulate operation to a path etc.

Thank you in advance :)

@lafrech
Copy link
Member

lafrech commented Feb 15, 2024

I'm pretty sure it works by just passing

requestBody ="MyItem"

the string being interpreted as a ref (whether or not it is actually defined)

@Kostadin-Ivanov
Copy link
Author

Kostadin-Ivanov commented Feb 15, 2024

If I pass
requestBody = "MyItem"

I get error:
File "c:\workspace\.venv\Lib\site-packages\apispec\core.py", line 352, in _resolve_refs_in_request_body
for media_type in request_body["content"].values():
~~~~~~~~~~~~^^^^^^^^^^^
TypeError: string indices must be integers, not 'str'

From this code:

**def _resolve_refs_in_request_body(self, request_body) -> None:
    # requestBody is OpenAPI v3+
    for media_type in request_body["content"].values():
        self._resolve_schema(media_type)
        self._resolve_examples(media_type)**

which is from the .venv\Lib\site-packages\apispec\core.py

Sorry, i tried to add a screenshot but did not work for some reason.

I tracked the usage of: _resolve_refs_in_request_body and it is called by: _resolve_refs_in_operation, which is called by: resolve_refs_in_path, which then is called by the path() function, which is called by my code with: apiSpec.path(...) in the description of this ticket.

In other words, the flow is:
**
apiSpec.path(...)
-> resolve_refs_in_path
--> _resolve_refs_in_operation
---> _resolve_refs_in_request_body**

where the _resolve_refs_in_request_body is handling the passed "MyItem" string as dict and throws the above error.

@lafrech
Copy link
Member

lafrech commented Feb 15, 2024

Sorry, please try

requestBody = {
    "content": {"application/json": {"schema": "MyItem"}}
}

as in this test.

@Kostadin-Ivanov
Copy link
Author

Thank you, Jerome.

This example works fine, but my question was if could set it more like:
requestBody = "#/components/requestBodies/MyItem"
or
requestBody = { "$ref": "#/components/requestBodies/MyItem" }

which will then result in:
post:
description: Create MyItem
operationId: createMyItem
requestBody:
$ref: "#/components/requestBodies/MyItem"

@lafrech
Copy link
Member

lafrech commented Feb 15, 2024

I'm afraid the content type (application/json) must be specified by the user.

The fact that you don't need to pass the components/requestBody part is nice, though.

@Kostadin-Ivanov
Copy link
Author

Kostadin-Ivanov commented Feb 16, 2024

Thank you Jerome.

so, for the moment the requestBody needs to be setup with its schema definition like:
{
content:
application/json:
schema:
...
}

Thank you for your support on this and hopefully, the _resolve_refs_in_request_body function, and others similar to it, might be updated in such way that to handle exact schema parameters, like the one above and $ref like { "$ref": "#/components/requestBodies/MyItem" }.

I guess that you can close this ticket as "replied" :)

Thank you and have a great day.

@lafrech
Copy link
Member

lafrech commented Feb 16, 2024

I'm not sure I get it. You need to pass the content type because apispec can't guess this is JSON. In flask-smorest, for instance, things are more automatic because we define a default content type.

I don't see the point of passing the full component path, I think it is nice that apispec does that for you and you only need to pass the component name. If using marshmallow plugin, you may also pass the schema object and apispec resolves it for you.

@Kostadin-Ivanov
Copy link
Author

Sorry, I misunderstood you.

Your example:

 requestBody = {
     "content": {"application/json": {"schema": "MyItem"}}
 }

worked. However, it still added some kind of duplication in my generated OpenApi under each of my generated operations like:

post:
  requestBody:
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/MyItem'
put:
  requestBody:
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/MyItem'

in my generated OpenAPI file, where I am looking to get more like:

post:
  requestBody:
    $ref: '#/components/requestBodies/MyItem'
put:
  requestBody:
    $ref: '#/components/requestBodies/MyItem'

where the $ref is pointing to a pre-defined request body schema like the one below:

components:
  requestBodies:
    MyItem:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/MyItem'

Anyway, for the moment it does not seem to be achievable with this
requestBody = { "content": {"application/json": {"schema": "MyItem"}}} approach.

I will follow your example and see if I can find some other way to save 3 lines of text under each operation :)

Thank you again and have a great day :)

@lafrech
Copy link
Member

lafrech commented Feb 16, 2024

I didn't check but I'm not sure what you're trying to achieve is valid OpenAPI. You may want to double-check.

I wouldn't worry about the duplication in the generated spec file. However, code factoring is nice. You could define a function to add the content type for you. Or use a wrapper such as flask-smorest that does that and more.

@Kostadin-Ivanov
Copy link
Author

The swagger editor accepts the components:requestBodies:...

It is not perfect example, but here is one, of "MyItem".
my_item_example_openapi.json

Works fine on SWAGGER editor.

Thank you very much for your time and help on this question :)

@lafrech
Copy link
Member

lafrech commented Feb 16, 2024

You're totally right. I got confused. Sorry about that.

The example I picked from the tests resolves the schema in the request body, not the request body itself.

What you're trying to achieve is absolutely correct and in fact desirable.

It should work with

requestBody ="MyItem"

but this is apparently not implemented.

What would need to be done:

  • Add
    "request_body": "requestBodies",

to utils.COMPONENT_SUBSECTIONS.

  • Add a Components.request_body method to register request bodies in components section.

  • Add a call to get_ref in resolve_refs_in_operation like

        if "requestBody" in operation:
            operation["requestBody"] = self.get_ref("request_body", operation["requestBody"])
            self._resolve_refs_in_request_body(operation["requestBody"])

I haven't been looking to this code for a while so I may be mistaken again but I think the solution should be along these lines.

Can't look into it right now but anyone, feel free to work on this.

@lafrech lafrech reopened this Feb 16, 2024
@lafrech lafrech changed the title How can I create an Operation with a ref to shared requestBody Add requestBody component Feb 16, 2024
@Kostadin-Ivanov
Copy link
Author

Kostadin-Ivanov commented Feb 16, 2024

This is great, Jérôme :)

Thank you very much for this detailed guidance. I will try it and will let you know with results.

Thanks again and hopefully, we will get the easy solution like:

requestBody = "MyItem"

in the future updates :)

I sorry, but I can do these additions in my code by somehow inject / extend the ApiSpec functionality, or these updates need to be added to the ApiSpec core functionality in the future?

@lafrech
Copy link
Member

lafrech commented Feb 19, 2024

Yes, the feature should be added to apispec as per the "what's to be done" section of my comment.

You may try to achieve this in a local branch and propose a PR. (Feel free to ask if you're not used to do that.)

Thanks.

@Kostadin-Ivanov
Copy link
Author

Thank you very much.

I will see if I have the time and might try to do something on it.
For this I will check the ApiSpec contributing guidelines.

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

No branches or pull requests

2 participants