Repository: python-openapi/openapi-core (language: python)

Issue:
[Bug]: Maximum recursing depth exceeded
### Actual Behavior

When using the openapi spec for [Azure OpenAI](https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cognitiveservices/data-plane/AzureOpenAI/inference/preview/2024-02-15-preview/inference.json) with any supported body results in a `RecursionError: maximum recursion depth exceeded.` and the code execution stops

I had code as part of a fastapi, but kept running into this issue so I isolated the code in a small project for which the link is in the repro steps.

Also other specs for versions of the same API have this issue. All of them are valid when parsed on https://validator.swagger.io/
The list of all specs can be found [here](https://github.com/Azure/azure-rest-api-specs/tree/main/specification/cognitiveservices/data-plane/AzureOpenAI/inference)

the full error I get is posted below.

I hope this helps!

```
C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\openapi_core\security\providers.py:20: UserWarning: Unsupported scheme type
  warnings.warn("Unsupported scheme type")
Traceback (most recent call last):
  File "c:\Code\gaia-retrieval-gateway-poc\testing\specvalidator.py", line 29, in <module>
    result = openapi.unmarshal_request(openapi_request)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\openapi_core\app.py", line 398, in unmarshal_request
    return self.unmarshal_apicall_request(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\openapi_core\app.py", line 413, in unmarshal_apicall_request
    return self.request_unmarshaller.unmarshal(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\openapi_core\unmarshalling\request\unmarshallers.py", line 249, in unmarshal
    return self._unmarshal(request, operation, path)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\openapi_core\unmarshalling\request\unmarshallers.py", line 156, in _unmarshal
    body = self._get_body(
           ^^^^^^^^^^^^^^^
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\openapi_core\validation\decorators.py", line 31, in wrapper
    return f(*args, **kwds)
           ^^^^^^^^^^^^^^^^
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\openapi_core\validation\request\validators.py", line 268, in _get_body   
    value, _ = self._get_content_and_schema(raw_body, content, mimetype)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\openapi_core\unmarshalling\unmarshallers.py", line 118, in _get_content_and_schema
    return self._unmarshal_schema(schema, casted), schema
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\openapi_core\unmarshalling\unmarshallers.py", line 95, in _unmarshal_schema
    return unmarshaller.unmarshal(value)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\openapi_core\unmarshalling\schemas\unmarshallers.py", line 249, in unmarshal
    typed = type_unmarshaller(value)
            ^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\openapi_core\unmarshalling\schemas\unmarshallers.py", line 51, in __call__
    properties = self._unmarshal_properties(value)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\openapi_core\unmarshalling\schemas\unmarshallers.py", line 94, in _unmarshal_properties
    )._unmarshal_properties(value, schema_only=True)
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\openapi_core\unmarshalling\schemas\unmarshallers.py", line 107, in _unmarshal_properties
    ).unmarshal(prop_value)
      ^^^^^^^^^^^^^^^^^^^^^
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\openapi_core\unmarshalling\schemas\unmarshallers.py", line 249, in unmarshal
    typed = type_unmarshaller(value)
            ^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\openapi_core\unmarshalling\schemas\unmarshallers.py", line 143, in __call__
    return unmarshaller(value)
           ^^^^^^^^^^^^^^^^^^^
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\openapi_core\unmarshalling\schemas\unmarshallers.py", line 143, in __call__
    return unmarshaller(value)
           ^^^^^^^^^^^^^^^^^^^
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\openapi_core\unmarshalling\schemas\unmarshallers.py", line 143, in __call__
    return unmarshaller(value)
           ^^^^^^^^^^^^^^^^^^^
  [Previous line repeated 976 more times]
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\openapi_core\unmarshalling\schemas\unmarshallers.py", line 139, in __call__
    primitive_type = self.schema_validator.get_primitive_type(value)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\openapi_core\validation\schemas\validators.py", line 82, in get_primitive_type
    schema_types = self.schema.getkey("type")
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\pathable\paths.py", line 177, in getkey
    with self.open() as d:
  File "C:\Users\M63H578\AppData\Local\Programs\Python\Python312\Lib\contextlib.py", line 137, in __enter__
    return next(self.gen)
           ^^^^^^^^^^^^^^
  File "C:\Code\gaia-retrieval-gateway-poc\venv\Lib\site-packages\jsonschema_path\paths.py", line 119, in open
    with self.resolve() as resolved:
         ^^^^^^^^^^^^^^
  File "C:\Users\M63H578\AppData\Local\Programs\Python\Python312\Lib\contextlib.py", line 301, in helper
    return _GeneratorContextManager(func, args, kwds)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\M63H578\AppData\Local\Programs\Python\Python312\Lib\contextlib.py", line 105, in __init__
    self.gen = func(*args, **kwds)
               ^^^^^^^^^^^^^^^^^^^
RecursionError: maximum recursion depth exceeded
```

### Expected Behavior

The validation to succeed when passing a valid body in the request.

### Steps to Reproduce

I created a temporary repo with my [code](https://github.com/mpriem/openapi-core-recursion-error/tree/main), so you can reproduce.

Please let me know if there is anything else you need. Thanks in advance!

### OpenAPI Core Version

0.19.2

### OpenAPI Core Integration

none but had the same issue with fastapi

### Affected Area(s)

unmarschaling

### References

N/A

### Anything else we need to know?

_No response_

### Would you like to implement a fix?

None


Discussion / hints:
Hi @mpriem thanks for the report, I see the problem is with `None` value in combination with nullable=True in subschema for OpenAPI 3.0; we don't catch the case and `AnyUnmarshaller` [tries to find the type recursively](https://github.com/python-openapi/openapi-core/blob/master/openapi_core/unmarshalling/schemas/unmarshallers.py#L143).

Fix patch:
diff --git a/openapi_core/unmarshalling/schemas/unmarshallers.py b/openapi_core/unmarshalling/schemas/unmarshallers.py
index 53ddcc34..1df9ed09 100644
--- a/openapi_core/unmarshalling/schemas/unmarshallers.py
+++ b/openapi_core/unmarshalling/schemas/unmarshallers.py
@@ -137,6 +137,9 @@ def _unmarshal_properties(
 class MultiTypeUnmarshaller(PrimitiveUnmarshaller):
     def __call__(self, value: Any) -> Any:
         primitive_type = self.schema_validator.get_primitive_type(value)
+        # OpenAPI 3.0: handle no type for None
+        if primitive_type is None:
+            return None
         unmarshaller = self.schema_unmarshaller.get_type_unmarshaller(
             primitive_type
         )
@@ -247,6 +250,9 @@ def unmarshal(self, value: Any) -> Any:
         schema_type = self.schema.getkey("type")
         type_unmarshaller = self.get_type_unmarshaller(schema_type)
         typed = type_unmarshaller(value)
+        # skip finding format for None
+        if typed is None:
+            return None
         schema_format = self.find_format(value)
         if schema_format is None:
             return typed
diff --git a/openapi_core/validation/schemas/validators.py b/openapi_core/validation/schemas/validators.py
index c822a82f..7a764e31 100644
--- a/openapi_core/validation/schemas/validators.py
+++ b/openapi_core/validation/schemas/validators.py
@@ -94,6 +94,7 @@ def get_primitive_type(self, value: Any) -> Optional[str]:
                 continue
             assert isinstance(schema_type, (str, type(None)))
             return schema_type
+        # OpenAPI 3.0: None is not a primitive type so None value will not find any type
         return None
 
     def iter_valid_schemas(self, value: Any) -> Iterator[SchemaPath]:


Test patch:
diff --git a/tests/integration/unmarshalling/test_unmarshallers.py b/tests/integration/unmarshalling/test_unmarshallers.py
index 764fc3af..54e944a3 100644
--- a/tests/integration/unmarshalling/test_unmarshallers.py
+++ b/tests/integration/unmarshalling/test_unmarshallers.py
@@ -1840,6 +1840,25 @@ def test_object_property_nullable(self, unmarshallers_factory):
 
         assert result == value
 
+    def test_subschema_nullable(self, unmarshallers_factory):
+        schema = {
+            "oneOf": [
+                {
+                    "type": "integer",
+                },
+                {
+                    "nullable": True,
+                },
+            ]
+        }
+        spec = SchemaPath.from_dict(schema)
+        unmarshaller = unmarshallers_factory.create(spec)
+        value = None
+
+        result = unmarshaller.unmarshal(value)
+
+        assert result is None
+
 
 class TestOAS30RequestSchemaUnmarshallersFactory(
     BaseTestOASSchemaUnmarshallersFactoryCall,
@@ -2086,3 +2105,22 @@ def test_any_null(self, unmarshallers_factory):
         result = unmarshaller.unmarshal(None)
 
         assert result is None
+
+    def test_subschema_null(self, unmarshallers_factory):
+        schema = {
+            "oneOf": [
+                {
+                    "type": "integer",
+                },
+                {
+                    "type": "null",
+                },
+            ]
+        }
+        spec = SchemaPath.from_dict(schema)
+        unmarshaller = unmarshallers_factory.create(spec)
+        value = None
+
+        result = unmarshaller.unmarshal(value)
+
+        assert result is None