diff --git a/HISTORY.md b/HISTORY.md index 29e94f8e..9f0afac4 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,8 @@ # cloudpathlib Changelog +## UNRELEASED +- Added support for Pydantic serialization (Issue [#537](https://github.com/drivendataorg/cloudpathlib/issues/537), PR [#538](https://github.com/drivendataorg/cloudpathlib/pull/538)) + ## v0.23.0 (2025-10-07) - Added support for Python 3.14 (Issue [#529](https://github.com/drivendataorg/cloudpathlib/issues/529), PR [#530](https://github.com/drivendataorg/cloudpathlib/pull/530)) diff --git a/cloudpathlib/anypath.py b/cloudpathlib/anypath.py index dbab9db9..ff6a0301 100644 --- a/cloudpathlib/anypath.py +++ b/cloudpathlib/anypath.py @@ -48,6 +48,10 @@ def __get_pydantic_core_schema__(cls, _source_type: Any, _handler): return core_schema.no_info_after_validator_function( cls.validate, core_schema.any_schema(), + serialization=core_schema.plain_serializer_function_ser_schema( + lambda x: str(x), + return_schema=core_schema.str_schema(), + ), ) except ImportError: return None diff --git a/cloudpathlib/cloudpath.py b/cloudpathlib/cloudpath.py index fa925b5b..92e46323 100644 --- a/cloudpathlib/cloudpath.py +++ b/cloudpathlib/cloudpath.py @@ -1593,6 +1593,10 @@ def __get_pydantic_core_schema__(cls, _source_type: Any, _handler): return core_schema.no_info_after_validator_function( cls.validate, core_schema.any_schema(), + serialization=core_schema.plain_serializer_function_ser_schema( + lambda x: str(x), + return_schema=core_schema.str_schema(), + ), ) except ImportError: return None diff --git a/docs/docs/integrations.md b/docs/docs/integrations.md index 9c059057..4374c0af 100644 --- a/docs/docs/integrations.md +++ b/docs/docs/integrations.md @@ -14,6 +14,8 @@ class MyModel(BaseModel): inst = MyModel(s3_file="s3://mybucket/myfile.txt") inst.s3_file #> S3Path('s3://mybucket/myfile.txt') +inst.model_dump_json() +#> '{"s3_file":"s3://mybucket/myfile.txt"}' ``` This also works with the `AnyPath` polymorphic class. Inputs will get dispatched and instantiated as the appropriate class. @@ -32,6 +34,23 @@ fancy1.path fancy2 = FancyModel(path="mydir/myfile.txt") fancy2.path #> PosixPath('mydir/myfile.txt') +fancy2.model_dump_json() +#> '{"path":"mydir/myfile.txt"}' +``` + +As seen above, the default serialization uses the URI but Pydantic supports custom serializers. + +```python +from typing import Annotated +from cloudpathlib import S3Path +from pydantic import BaseModel, PlainSerializer + +class MyModel(BaseModel): + s3_file: Annotated[S3Path, PlainSerializer(lambda x: x.as_url())] + +inst = MyModel(s3_file="s3://mybucket/myfile.txt") +inst.model_dump_json() +#> '{"s3_file":"https://mybucket.s3.amazonaws.com/myfile.txt"}' ``` --- diff --git a/tests/test_integrations.py b/tests/test_integrations.py index 65d211b0..ca6effb6 100644 --- a/tests/test_integrations.py +++ b/tests/test_integrations.py @@ -14,9 +14,11 @@ class PydanticModel(BaseModel): obj = PydanticModel(cloud_path=cp) assert obj.cloud_path == cp + assert obj.model_dump_json() == f'{{"cloud_path":"{cp}"}}' obj = PydanticModel(cloud_path=str(cp)) assert obj.cloud_path == cp + assert obj.model_dump_json() == f'{{"cloud_path":"{cp}"}}' with pytest.raises(ValidationError): _ = PydanticModel(cloud_path=0) @@ -30,9 +32,11 @@ class PydanticModel(BaseModel): obj = PydanticModel(any_path=cp) assert obj.any_path == cp + assert obj.model_dump_json() == f'{{"any_path":"{cp}"}}' obj = PydanticModel(any_path=str(cp)) assert obj.any_path == cp + assert obj.model_dump_json() == f'{{"any_path":"{cp}"}}' obj = PydanticModel(any_path=Path("a/b/c")) assert obj.any_path == Path("a/b/c")