Repository: PrefectHQ/prefect

Issue:
AWS Client Parameters are stored as a dictionary and throw an error when assigned to an S3 bucket through the UI
### 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

First I create an S3 block and an AWS Credentials block from the SDK. If I assign the credentials block to the S3 bucket in the UI, the Client Parameters are incorrectly stored as a dictionary. This leads to an error any time I try to use the S3 bucket I created.

I can see from the output of my loaded S3 bucket `loaded_s3 = S3Bucket.load("s3-from-code")` that aws_client_parameters is stored as a dictionary rather than an instance of AwsClientParameters as it should be:

`AwsCredentials(region_name=None, profile_name=None, aws_access_key_id=None, aws_session_token=None, **aws_client_parameters={'config': None, 'verify': True, 'use_ssl': True, 'api_version': None, 'endpoint_url': None, 'verify_cert_path': None}**, aws_secret_access_key=None)`

Which leads to `TypeError: unhashable type: 'dict'` whenever I try to work with the S3 bucket.

### Reproduction
1. Create an S3 block from the SDK:
```python3
block = S3Bucket(bucket_name="se-demo-flow-code-store", bucket_path="se-demo-flow-code-store/weather/")
block.save(name="s3-from-code", overwrite=True)`
```

2. Create an AWS Credentials block from the SDK:
```python3
AwsCredentials(
    aws_access_key_id="access_key",
    aws_secret_access_key="secret_key",
    aws_session_token="session_token",
    region_name="us-east-2"
).save("aws-credentials-from-code", overwrite=True)`
```

3. Assign the credentials block to S3 in the UI
![Screenshot 2024-05-14 at 4 51 06 PM](https://github.com/PrefectHQ/prefect/assets/43013545/e75b268f-8ee5-4a39-be1b-536046956274)

4. Load in S3 Bucket:
```python3
loaded_s3 = S3Bucket.load("s3-from-code")`
```

5. Try to copy an object to the S3 bucket:
```python3
loaded_s3.copy_object(
    "my_folder/notes.txt",
    "my_folder/notes_copy.txt"
)`
```


### Error

```python3
Traceback (most recent call last):
  File "/Users/mihir/Documents/Code/create_aws_creds.py", line 18, in <module>
    loaded_s3.copy_object(
  File "/Users/mihir/anaconda3/envs/updated-prefect/lib/python3.11/site-packages/prefect/events/instrument.py", line 73, in inner
    raise exc
  File "/Users/mihir/anaconda3/envs/updated-prefect/lib/python3.11/site-packages/prefect/events/instrument.py", line 70, in inner
    return function(self, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mihir/anaconda3/envs/updated-prefect/lib/python3.11/site-packages/prefect/utilities/asyncutils.py", line 259, in coroutine_wrapper
    return call()
           ^^^^^^
  File "/Users/mihir/anaconda3/envs/updated-prefect/lib/python3.11/site-packages/prefect/_internal/concurrency/calls.py", line 432, in __call__
    return self.result()
           ^^^^^^^^^^^^^
  File "/Users/mihir/anaconda3/envs/updated-prefect/lib/python3.11/site-packages/prefect/_internal/concurrency/calls.py", line 318, in result
    return self.future.result(timeout=timeout)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mihir/anaconda3/envs/updated-prefect/lib/python3.11/site-packages/prefect/_internal/concurrency/calls.py", line 179, in result
    return self.__get_result()
           ^^^^^^^^^^^^^^^^^^^
  File "/Users/mihir/anaconda3/envs/updated-prefect/lib/python3.11/concurrent/futures/_base.py", line 401, in __get_result
    raise self._exception
  File "/Users/mihir/anaconda3/envs/updated-prefect/lib/python3.11/site-packages/prefect/_internal/concurrency/calls.py", line 389, in _run_async
    result = await coro
             ^^^^^^^^^^
  File "/Users/mihir/anaconda3/envs/updated-prefect/lib/python3.11/site-packages/prefect_aws/s3.py", line 1236, in copy_object
    s3_client = self.credentials.get_s3_client()
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mihir/anaconda3/envs/updated-prefect/lib/python3.11/site-packages/prefect_aws/credentials.py", line 180, in get_s3_client
    return self.get_client(client_type=ClientType.S3)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mihir/anaconda3/envs/updated-prefect/lib/python3.11/site-packages/prefect_aws/credentials.py", line 171, in get_client
    return _get_client_cached(ctx=self, client_type=client_type)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mihir/anaconda3/envs/updated-prefect/lib/python3.11/site-packages/prefect_aws/credentials.py", line 122, in __hash__
    hash(self.aws_client_parameters),
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: unhashable type: 'dict'
```


### Versions

```Text
Version:             2.16.9
API version:         0.8.4
Python version:      3.11.8
Git commit:          083def52
Built:               Thu, Apr 4, 2024 3:11 PM
OS/Arch:             darwin/arm64
Profile:             default
Server type:         cloud
```


### Additional context

_No response_


Discussion:
I ran into the same problem (v 2.19.0). Downgrading to v2.16.8 solved the issue.
I faced the same issue when changed the persistence result configuration from a `S3` Block to a `S3Bucket` Block:
`prefect config set PREFECT_DEFAULT_RESULT_STORAGE_BLOCK=s3-bucket/my-bucket`

```
  File "/home/root/ve/lib/python3.11/site-packages/prefect_aws/credentials.py", line 121, in __hash__
    hash(self.aws_client_parameters),
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: unhashable type: 'dict'
```

Resolving patch:
diff --git a/src/integrations/prefect-aws/prefect_aws/s3.py b/src/integrations/prefect-aws/prefect_aws/s3.py
--- a/src/integrations/prefect-aws/prefect_aws/s3.py
+++ b/src/integrations/prefect-aws/prefect_aws/s3.py
@@ -5,19 +5,20 @@
 import os
 import uuid
 from pathlib import Path
-from typing import Any, BinaryIO, Dict, List, Optional, Union
+from typing import Any, BinaryIO, Dict, List, Optional, Union, get_args
 
 import boto3
 from botocore.paginate import PageIterator
 from botocore.response import StreamingBody
-from pydantic import Field
+from pydantic import Field, field_validator
 
 from prefect import task
-from prefect.blocks.abstract import ObjectStorageBlock
+from prefect.blocks.abstract import CredentialsBlock, ObjectStorageBlock
 from prefect.filesystems import WritableDeploymentStorage, WritableFileSystem
 from prefect.logging import get_run_logger
 from prefect.utilities.asyncutils import run_sync_in_worker_thread, sync_compatible
 from prefect.utilities.filesystem import filter_files
+from prefect.utilities.pydantic import lookup_type
 from prefect_aws import AwsCredentials, MinIOCredentials
 from prefect_aws.client_parameters import AwsClientParameters
 
@@ -421,6 +422,42 @@ class S3Bucket(WritableFileSystem, WritableDeploymentStorage, ObjectStorageBlock
         ),
     )
 
+    @field_validator("credentials", mode="before")
+    def validate_credentials(cls, value, field):
+        if isinstance(value, dict):
+            # There is an issue with pydantic and nested blocks with union
+            # types, in this case credentials is affected by it. What happens
+            # is that the credentials block appears to be correctly initialized
+            # but when it's attached to the parent block it's an
+            # _uninitialized_ instance without the field attributes.
+
+            # This validator is a workaround to check for the correct type
+            # or fallback to iterating over the possible credential types
+            # and trying to initialize them.
+
+            block_type_slug = value.pop("block_type_slug", None)
+            if block_type_slug:
+                credential_classes = (
+                    lookup_type(CredentialsBlock, dispatch_key=block_type_slug),
+                )
+            else:
+                credential_classes = get_args(
+                    cls.model_fields["credentials"].annotation
+                )
+
+            for credentials_cls in credential_classes:
+                try:
+                    return credentials_cls(**value)  # type: ignore
+                except ValueError:
+                    pass
+
+            valid_classes = ", ".join(c.__name__ for c in credential_classes)
+            raise ValueError(
+                f"Invalid credentials data: does not match any credential type. Valid types: {valid_classes}"
+            )
+
+        return value
+
     # Property to maintain compatibility with storage block based deployments
     @property
     def basepath(self) -> str:


Test patch:
diff --git a/src/integrations/prefect-aws/tests/test_s3.py b/src/integrations/prefect-aws/tests/test_s3.py
--- a/src/integrations/prefect-aws/tests/test_s3.py
+++ b/src/integrations/prefect-aws/tests/test_s3.py
@@ -1102,3 +1102,12 @@ def test_move_subpaths(
             to_bucket=to_bucket,
         )
         assert key == expected_path
+
+    def test_round_trip_default_credentials(self):
+        # Regression test for
+        # https://github.com/PrefectHQ/prefect/issues/13349
+        S3Bucket(bucket_name="round-trip-bucket").save("round-tripper")
+        loaded = S3Bucket.load("round-tripper")
+        assert hasattr(
+            loaded.credentials, "aws_access_key_id"
+        ), "`credentials` were not properly initialized"