Repository: openapi-generators/openapi-python-client (language: python)

Issue:
fix: OpenAPI schema validation issues.
Based on (and replacing) #426.

BREAKING CHANGE: Validation of OpenAPI documents is now more strict.


Discussion / hints:
# [Codecov](https://codecov.io/gh/openapi-generators/openapi-python-client/pull/568?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#568](https://codecov.io/gh/openapi-generators/openapi-python-client/pull/568?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (1bbbc98) into [main](https://codecov.io/gh/openapi-generators/openapi-python-client/commit/821dac8edde8e050668d06f0402c985bc5d35dc7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (821dac8) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/openapi-generators/openapi-python-client/pull/568/graphs/tree.svg?width=650&height=150&src=pr&token=oQ6ze3blGT&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)](https://codecov.io/gh/openapi-generators/openapi-python-client/pull/568?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)

```diff
@@            Coverage Diff            @@
##              main      #568   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           48        49    +1     
  Lines         1674      1713   +39     
=========================================
+ Hits          1674      1713   +39     
```


| [Impacted Files](https://codecov.io/gh/openapi-generators/openapi-python-client/pull/568?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [...penapi\_python\_client/parser/properties/\_\_init\_\_.py](https://codecov.io/gh/openapi-generators/openapi-python-client/pull/568/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-b3BlbmFwaV9weXRob25fY2xpZW50L3BhcnNlci9wcm9wZXJ0aWVzL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [...\_python\_client/parser/properties/model\_property.py](https://codecov.io/gh/openapi-generators/openapi-python-client/pull/568/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-b3BlbmFwaV9weXRob25fY2xpZW50L3BhcnNlci9wcm9wZXJ0aWVzL21vZGVsX3Byb3BlcnR5LnB5) | `100.00% <100.00%> (ø)` | |
| [...\_client/schema/openapi\_schema\_pydantic/\_\_init\_\_.py](https://codecov.io/gh/openapi-generators/openapi-python-client/pull/568/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-b3BlbmFwaV9weXRob25fY2xpZW50L3NjaGVtYS9vcGVuYXBpX3NjaGVtYV9weWRhbnRpYy9fX2luaXRfXy5weQ==) | `100.00% <100.00%> (ø)` | |
| [...\_client/schema/openapi\_schema\_pydantic/callback.py](https://codecov.io/gh/openapi-generators/openapi-python-client/pull/568/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-b3BlbmFwaV9weXRob25fY2xpZW50L3NjaGVtYS9vcGVuYXBpX3NjaGVtYV9weWRhbnRpYy9jYWxsYmFjay5weQ==) | `100.00% <100.00%> (ø)` | |
| [...lient/schema/openapi\_schema\_pydantic/components.py](https://codecov.io/gh/openapi-generators/openapi-python-client/pull/568/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-b3BlbmFwaV9weXRob25fY2xpZW50L3NjaGVtYS9vcGVuYXBpX3NjaGVtYV9weWRhbnRpYy9jb21wb25lbnRzLnB5) | `100.00% <100.00%> (ø)` | |
| [...n\_client/schema/openapi\_schema\_pydantic/contact.py](https://codecov.io/gh/openapi-generators/openapi-python-client/pull/568/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-b3BlbmFwaV9weXRob25fY2xpZW50L3NjaGVtYS9vcGVuYXBpX3NjaGVtYV9weWRhbnRpYy9jb250YWN0LnB5) | `100.00% <100.00%> (ø)` | |
| [...nt/schema/openapi\_schema\_pydantic/discriminator.py](https://codecov.io/gh/openapi-generators/openapi-python-client/pull/568/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-b3BlbmFwaV9weXRob25fY2xpZW50L3NjaGVtYS9vcGVuYXBpX3NjaGVtYV9weWRhbnRpYy9kaXNjcmltaW5hdG9yLnB5) | `100.00% <100.00%> (ø)` | |
| [...\_client/schema/openapi\_schema\_pydantic/encoding.py](https://codecov.io/gh/openapi-generators/openapi-python-client/pull/568/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-b3BlbmFwaV9weXRob25fY2xpZW50L3NjaGVtYS9vcGVuYXBpX3NjaGVtYV9weWRhbnRpYy9lbmNvZGluZy5weQ==) | `100.00% <100.00%> (ø)` | |
| [...n\_client/schema/openapi\_schema\_pydantic/example.py](https://codecov.io/gh/openapi-generators/openapi-python-client/pull/568/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-b3BlbmFwaV9weXRob25fY2xpZW50L3NjaGVtYS9vcGVuYXBpX3NjaGVtYV9weWRhbnRpYy9leGFtcGxlLnB5) | `100.00% <100.00%> (ø)` | |
| [.../openapi\_schema\_pydantic/external\_documentation.py](https://codecov.io/gh/openapi-generators/openapi-python-client/pull/568/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-b3BlbmFwaV9weXRob25fY2xpZW50L3NjaGVtYS9vcGVuYXBpX3NjaGVtYV9weWRhbnRpYy9leHRlcm5hbF9kb2N1bWVudGF0aW9uLnB5) | `100.00% <100.00%> (ø)` | |
| ... and [20 more](https://codecov.io/gh/openapi-generators/openapi-python-client/pull/568/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/openapi-generators/openapi-python-client/pull/568?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/openapi-generators/openapi-python-client/pull/568?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Last update [821dac8...1bbbc98](https://codecov.io/gh/openapi-generators/openapi-python-client/pull/568?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).


Fix patch:
diff --git a/openapi_python_client/schema/openapi_schema_pydantic/operation.py b/openapi_python_client/schema/openapi_schema_pydantic/operation.py
index 19c78c6d5..51d84e734 100644
--- a/openapi_python_client/schema/openapi_schema_pydantic/operation.py
+++ b/openapi_python_client/schema/openapi_schema_pydantic/operation.py
@@ -5,6 +5,9 @@
 from .callback import Callback
 from .external_documentation import ExternalDocumentation
 from .parameter import Parameter
+
+# Required to update forward ref after object creation, as this is not imported yet
+from .path_item import PathItem  # pylint: disable=unused-import
 from .reference import Reference
 from .request_body import RequestBody
 from .responses import Responses
@@ -79,3 +82,7 @@ class Config:  # pylint: disable=missing-class-docstring
                 }
             ]
         }
+
+
+# PathItem in Callback uses Operation, so we need to update forward refs due to circular dependency
+Operation.update_forward_refs()
diff --git a/openapi_python_client/schema/openapi_schema_pydantic/path_item.py b/openapi_python_client/schema/openapi_schema_pydantic/path_item.py
index af1a1a6a3..9fc51eb85 100644
--- a/openapi_python_client/schema/openapi_schema_pydantic/path_item.py
+++ b/openapi_python_client/schema/openapi_schema_pydantic/path_item.py
@@ -2,7 +2,6 @@
 
 from pydantic import BaseModel, Extra, Field
 
-from .operation import Operation
 from .parameter import Parameter
 from .reference import Reference
 from .server import Server
@@ -23,14 +22,14 @@ class PathItem(BaseModel):
     ref: Optional[str] = Field(default=None, alias="$ref")
     summary: Optional[str] = None
     description: Optional[str] = None
-    get: Optional[Operation] = None
-    put: Optional[Operation] = None
-    post: Optional[Operation] = None
-    delete: Optional[Operation] = None
-    options: Optional[Operation] = None
-    head: Optional[Operation] = None
-    patch: Optional[Operation] = None
-    trace: Optional[Operation] = None
+    get: Optional["Operation"] = None
+    put: Optional["Operation"] = None
+    post: Optional["Operation"] = None
+    delete: Optional["Operation"] = None
+    options: Optional["Operation"] = None
+    head: Optional["Operation"] = None
+    patch: Optional["Operation"] = None
+    trace: Optional["Operation"] = None
     servers: Optional[List[Server]] = None
     parameters: Optional[List[Union[Parameter, Reference]]] = None
 
@@ -70,3 +69,9 @@ class Config:  # pylint: disable=missing-class-docstring
                 }
             ]
         }
+
+
+# Operation uses PathItem via Callback, so we need late import and to update forward refs due to circular dependency
+from .operation import Operation  # pylint: disable=wrong-import-position unused-import
+
+PathItem.update_forward_refs()


Test patch:
diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py
index 8924dfa7a..13120943a 100644
--- a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py
+++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py
@@ -3,6 +3,7 @@
 import types
 
 from . import (
+    callback_test,
     defaults_tests_defaults_post,
     get_basic_list_of_booleans,
     get_basic_list_of_floats,
@@ -150,3 +151,10 @@ def token_with_cookie_auth_token_with_cookie_get(cls) -> types.ModuleType:
         Test optional cookie parameters
         """
         return token_with_cookie_auth_token_with_cookie_get
+
+    @classmethod
+    def callback_test(cls) -> types.ModuleType:
+        """
+        Try sending a request related to a callback
+        """
+        return callback_test
diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/callback_test.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/callback_test.py
new file mode 100644
index 000000000..e76778af3
--- /dev/null
+++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/callback_test.py
@@ -0,0 +1,152 @@
+from typing import Any, Dict, Optional, Union, cast
+
+import httpx
+
+from ...client import Client
+from ...models.a_model import AModel
+from ...models.http_validation_error import HTTPValidationError
+from ...types import Response
+
+
+def _get_kwargs(
+    *,
+    client: Client,
+    json_body: AModel,
+) -> Dict[str, Any]:
+    url = "{}/tests/callback".format(client.base_url)
+
+    headers: Dict[str, str] = client.get_headers()
+    cookies: Dict[str, Any] = client.get_cookies()
+
+    json_json_body = json_body.to_dict()
+
+    return {
+        "method": "post",
+        "url": url,
+        "headers": headers,
+        "cookies": cookies,
+        "timeout": client.get_timeout(),
+        "json": json_json_body,
+    }
+
+
+def _parse_response(*, response: httpx.Response) -> Optional[Union[Any, HTTPValidationError]]:
+    if response.status_code == 200:
+        response_200 = cast(Any, response.json())
+        return response_200
+    if response.status_code == 422:
+        response_422 = HTTPValidationError.from_dict(response.json())
+
+        return response_422
+    return None
+
+
+def _build_response(*, response: httpx.Response) -> Response[Union[Any, HTTPValidationError]]:
+    return Response(
+        status_code=response.status_code,
+        content=response.content,
+        headers=response.headers,
+        parsed=_parse_response(response=response),
+    )
+
+
+def sync_detailed(
+    *,
+    client: Client,
+    json_body: AModel,
+) -> Response[Union[Any, HTTPValidationError]]:
+    """Path with callback
+
+     Try sending a request related to a callback
+
+    Args:
+        json_body (AModel): A Model for testing all the ways custom objects can be used
+
+    Returns:
+        Response[Union[Any, HTTPValidationError]]
+    """
+
+    kwargs = _get_kwargs(
+        client=client,
+        json_body=json_body,
+    )
+
+    response = httpx.request(
+        verify=client.verify_ssl,
+        **kwargs,
+    )
+
+    return _build_response(response=response)
+
+
+def sync(
+    *,
+    client: Client,
+    json_body: AModel,
+) -> Optional[Union[Any, HTTPValidationError]]:
+    """Path with callback
+
+     Try sending a request related to a callback
+
+    Args:
+        json_body (AModel): A Model for testing all the ways custom objects can be used
+
+    Returns:
+        Response[Union[Any, HTTPValidationError]]
+    """
+
+    return sync_detailed(
+        client=client,
+        json_body=json_body,
+    ).parsed
+
+
+async def asyncio_detailed(
+    *,
+    client: Client,
+    json_body: AModel,
+) -> Response[Union[Any, HTTPValidationError]]:
+    """Path with callback
+
+     Try sending a request related to a callback
+
+    Args:
+        json_body (AModel): A Model for testing all the ways custom objects can be used
+
+    Returns:
+        Response[Union[Any, HTTPValidationError]]
+    """
+
+    kwargs = _get_kwargs(
+        client=client,
+        json_body=json_body,
+    )
+
+    async with httpx.AsyncClient(verify=client.verify_ssl) as _client:
+        response = await _client.request(**kwargs)
+
+    return _build_response(response=response)
+
+
+async def asyncio(
+    *,
+    client: Client,
+    json_body: AModel,
+) -> Optional[Union[Any, HTTPValidationError]]:
+    """Path with callback
+
+     Try sending a request related to a callback
+
+    Args:
+        json_body (AModel): A Model for testing all the ways custom objects can be used
+
+    Returns:
+        Response[Union[Any, HTTPValidationError]]
+    """
+
+    return (
+        await asyncio_detailed(
+            client=client,
+            json_body=json_body,
+        )
+    ).parsed
diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json
index 746fb90e4..8298670fd 100644
--- a/end_to_end_tests/openapi.json
+++ b/end_to_end_tests/openapi.json
@@ -1173,6 +1173,62 @@
           }
         }
       }
+    },
+    "/tests/callback": {
+      "post": {
+        "tags": [
+          "tests"
+        ],
+        "summary": "Path with callback",
+        "description": "Try sending a request related to a callback",
+        "operationId": "callback_test",
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/AModel"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "Successful Response",
+            "content": {
+              "application/json": {
+                "schema": {}
+              }
+            }
+          },
+          "422": {
+            "description": "Validation Error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/HTTPValidationError"
+                }
+              }
+            }
+          }
+        },
+        "callbacks": {
+          "event": {
+            "callback": {
+              "post": {
+                "responses": {
+                  "200": {
+                    "description": "Success"
+                  },
+                  "503": {
+                    "description": "Unavailable"
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
     }
   },
   "components": {
diff --git a/tests/test_schema/test_open_api.py b/tests/test_schema/test_open_api.py
index e332e4fca..7e9b8be9d 100644
--- a/tests/test_schema/test_open_api.py
+++ b/tests/test_schema/test_open_api.py
@@ -14,3 +14,23 @@ def test_validate_version(version, valid):
     else:
         with pytest.raises(ValidationError):
             OpenAPI.parse_obj(data)
+
+
+def test_parse_with_callback():
+    data = {
+        "openapi": "3.0.1",
+        "info": {"title": "API with Callback", "version": ""},
+        "paths": {
+            "/create": {
+                "post": {
+                    "responses": {"200": {"description": "Success"}},
+                    "callbacks": {"event": {"callback": {"post": {"responses": {"200": {"description": "Success"}}}}}},
+                }
+            }
+        },
+    }
+
+    open_api = OpenAPI.parse_obj(data)
+    create_endpoint = open_api.paths["/create"]
+    assert "200" in create_endpoint.post.responses
+    assert "200" in create_endpoint.post.callbacks["event"]["callback"].post.responses