Repository: PrefectHQ/prefect

Issue:
Flow inputs annotated as container types crash with Pydantic V2
### First check

- [X] I added a descriptive title to this issue.
- [X] I used the GitHub search to find a similar issue and didn't find it.
- [X] I searched the Prefect documentation for this issue.
- [X] I checked that this issue is related to Prefect and not one of its dependencies.

### Bug summary

If I define a flow that has a typed container as an input, prefect will crash when it tries to deploy that flow using the `flow.serve` function. Downgrading pydantic to a version below major version 2 fixes the issue, but our project requires pydantic == 2.2.1

It looks like this issue might be related to this issue? https://github.com/PrefectHQ/prefect/issues/10506 but they're running a lower pydantic version, which should have worked here.

### Reproduction

```python3
from prefect import flow


@flow
def a_flow(arg1: list[str]) -> str:
    print(arg1)
    return ",".join(arg1)


if __name__ == "__main__":
    a_flow.serve(name="test-flow")
```


### Error

```python3
Traceback (most recent call last):
  File "/Users/big-n00b/local-repo/run_flow.py", line 5, in <module>
    def mini_flow(arg1: list[str]) -> str:
  File "/Users/big-n00b/Library/Caches/pypoetry/virtualenvs/my-project-RcQVLUoH-py3.10/lib/python3.10/site-packages/prefect/flows.py", line 1339, in flow
    Flow(
  File "/Users/big-n00b/Library/Caches/pypoetry/virtualenvs/my-project-RcQVLUoH-py3.10/lib/python3.10/site-packages/prefect/context.py", line 185, in __register_init__
    __init__(__self__, *args, **kwargs)
  File "/Users/big-n00b/Library/Caches/pypoetry/virtualenvs/my-project-RcQVLUoH-py3.10/lib/python3.10/site-packages/prefect/flows.py", line 299, in __init__
    self.parameters = parameter_schema(self.fn)
  File "/Users/big-n00b/Library/Caches/pypoetry/virtualenvs/my-project-RcQVLUoH-py3.10/lib/python3.10/site-packages/prefect/utilities/callables.py", line 322, in parameter_schema
    if HAS_PYDANTIC_V2 and has_v2_model_as_param(signature):
  File "/Users/big-n00b/Library/Caches/pypoetry/virtualenvs/my-project-RcQVLUoH-py3.10/lib/python3.10/site-packages/prefect/_internal/pydantic/v2_schema.py", line 22, in has_v2_model_as_param
    return any(
  File "/Users/big-n00b/Library/Caches/pypoetry/virtualenvs/my-project-RcQVLUoH-py3.10/lib/python3.10/site-packages/prefect/_internal/pydantic/v2_schema.py", line 24, in <genexpr>
    or (inspect.isclass(p.annotation) and issubclass(p.annotation, V2BaseModel))
  File "/Users/big-n00b/.pyenv/versions/3.10.12/lib/python3.10/abc.py", line 123, in __subclasscheck__
    return _abc_subclasscheck(cls, subclass)
TypeError: issubclass() arg 1 must be a class
```


### Versions

```Text
Version:       2.14.3
API version:     0.8.4
Python version:   3.10.12
Git commit:     f1ff9257
Built:        Thu, Nov 2, 2023 4:12 PM
OS/Arch:       darwin/arm64
Profile:       default
```


### Additional context

_No response_


Discussion:
Hi, @psktam, thanks for creating your first issue with Prefect! I am able to replicate your issue, as we only test Pydantic V2 compatibility with our highest/lowest Python versions (3.8 and 3.11). Are you able to upgrade to 3.11 by chance?

We will add this to our backlog!
I've also opened a [draft PR](https://github.com/PrefectHQ/prefect/pull/11150/files) to update our workflow so that we do test again 3.10 and 3.9 in future.

Resolving patch:
diff --git a/src/prefect/_internal/pydantic/v2_schema.py b/src/prefect/_internal/pydantic/v2_schema.py
--- a/src/prefect/_internal/pydantic/v2_schema.py
+++ b/src/prefect/_internal/pydantic/v2_schema.py
@@ -1,6 +1,7 @@
 # Note: This file should only be imported under a HAS_PYDANTIC_V2 flag
 
 import inspect
+import typing
 import typing as t
 
 import pendulum
@@ -17,13 +18,28 @@
 from prefect._internal.pydantic.schemas import GenerateEmptySchemaForUserClasses
 
 
+def _is_v2_model(v) -> bool:
+    if isinstance(v, V2BaseModel):
+        return True
+    try:
+        if inspect.isclass(v) and issubclass(v, V2BaseModel):
+            return True
+    except TypeError:
+        pass
+
+
 def has_v2_model_as_param(signature: inspect.Signature) -> bool:
     parameters = signature.parameters.values()
-    return any(
-        isinstance(p, V2BaseModel)
-        or (inspect.isclass(p.annotation) and issubclass(p.annotation, V2BaseModel))
-        for p in parameters
-    )
+    for p in parameters:
+        # check if this parameter is a v2 model
+        if _is_v2_model(p.annotation):
+            return True
+
+        # check if this parameter is a collection of types
+        for v in typing.get_args(p.annotation):
+            if _is_v2_model(v):
+                return True
+    return False
 
 
 def process_v2_params(


Test patch:
diff --git a/tests/runner/test_runner.py b/tests/runner/test_runner.py
--- a/tests/runner/test_runner.py
+++ b/tests/runner/test_runner.py
@@ -2,11 +2,13 @@
 import os
 import re
 import signal
+import sys
 import time
 from itertools import combinations
 from pathlib import Path
 from textwrap import dedent
 from time import sleep
+from typing import List
 from unittest.mock import MagicMock
 
 import anyio
@@ -175,6 +177,36 @@ async def test_serve_prints_help_message_on_startup(self, capsys):
         assert "tired-flow/test_runner" in captured.out
         assert "$ prefect deployment run [DEPLOYMENT_NAME]" in captured.out
 
+    is_python_38 = sys.version_info[:2] == (3, 8)
+
+    async def test_serve_typed_container_inputs_flow(self, capsys):
+        if self.is_python_38:
+
+            @flow
+            def type_container_input_flow(arg1: List[str]) -> str:
+                print(arg1)
+                return ",".join(arg1)
+
+        else:
+
+            @flow
+            def type_container_input_flow(arg1: list[str]) -> str:
+                print(arg1)
+                return ",".join(arg1)
+
+        await serve(
+            await type_container_input_flow.to_deployment(__file__),
+        )
+
+        captured = capsys.readouterr()
+
+        assert (
+            "Your deployments are being served and polling for scheduled runs!"
+            in captured.out
+        )
+        assert "type-container-input-flow/test_runner" in captured.out
+        assert "$ prefect deployment run [DEPLOYMENT_NAME]" in captured.out
+
     async def test_serve_can_create_multiple_deployments(
         self,
         prefect_client: PrefectClient,
diff --git a/tests/test_flows.py b/tests/test_flows.py
--- a/tests/test_flows.py
+++ b/tests/test_flows.py
@@ -1353,6 +1353,25 @@ def my_flow(x):
 
         assert my_flow(data) == data
 
+    is_python_38 = sys.version_info[:2] == (3, 8)
+
+    def test_type_container_flow_inputs(self):
+        if self.is_python_38:
+
+            @flow
+            def type_container_input_flow(arg1: List[str]) -> str:
+                print(arg1)
+                return ",".join(arg1)
+
+        else:
+
+            @flow
+            def type_container_input_flow(arg1: list[str]) -> str:
+                print(arg1)
+                return ",".join(arg1)
+
+        assert type_container_input_flow(["a", "b", "c"]) == "a,b,c"
+
     def test_subflow_parameters_can_be_unserializable_types(self):
         data = ParameterTestClass()