From 8c499654b46c6d3f28d3c4f73796259568f6bee8 Mon Sep 17 00:00:00 2001 From: Mattijs De Paepe Date: Thu, 20 Nov 2025 14:52:04 +0530 Subject: [PATCH 1/5] support pydantic serialization --- cloudpathlib/anypath.py | 4 ++++ cloudpathlib/cloudpath.py | 4 ++++ docs/docs/integrations.md | 23 +++++++++++++++++++++++ tests/test_integrations.py | 5 +++++ 4 files changed, 36 insertions(+) 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..17a6e150 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,27 @@ 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 + +def serialize_as_http(path: S3Path) -> str: + """Convert S3Path to HTTP URL""" + return f"https://{path.bucket}.s3.amazonaws.com/{path.key}" + +class MyModel(BaseModel): + s3_file: Annotated[S3Path, PlainSerializer(serialize_as_http)] + +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..225da086 100644 --- a/tests/test_integrations.py +++ b/tests/test_integrations.py @@ -14,9 +14,12 @@ 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 +33,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") From 3f3acc142b22d8e33511a78d26d8b902e11ecf27 Mon Sep 17 00:00:00 2001 From: Mattijs De Paepe Date: Thu, 20 Nov 2025 14:57:24 +0530 Subject: [PATCH 2/5] update HISTORY.md --- HISTORY.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 29e94f8e..051b5b4f 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 []()) + ## 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)) From 9233c102922210708aaebfe6031b1828ebf70364 Mon Sep 17 00:00:00 2001 From: Mattijs De Paepe Date: Thu, 20 Nov 2025 20:56:16 +0000 Subject: [PATCH 3/5] add PR num to HISTORY.md --- HISTORY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 051b5b4f..9f0afac4 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,7 +1,7 @@ # cloudpathlib Changelog ## UNRELEASED -- Added support for Pydantic serialization (Issue [#537](https://github.com/drivendataorg/cloudpathlib/issues/537), PR []()) +- 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) From 7ce0470e30649a65023da176fc4850954785790d Mon Sep 17 00:00:00 2001 From: Mattijs De Paepe Date: Thu, 20 Nov 2025 21:00:20 +0000 Subject: [PATCH 4/5] black change --- tests/test_integrations.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_integrations.py b/tests/test_integrations.py index 225da086..ca6effb6 100644 --- a/tests/test_integrations.py +++ b/tests/test_integrations.py @@ -20,7 +20,6 @@ class PydanticModel(BaseModel): assert obj.cloud_path == cp assert obj.model_dump_json() == f'{{"cloud_path":"{cp}"}}' - with pytest.raises(ValidationError): _ = PydanticModel(cloud_path=0) From 7e8f4587183f6bf5722aaf7eb27a521243da3759 Mon Sep 17 00:00:00 2001 From: Mattijs De Paepe Date: Thu, 20 Nov 2025 21:02:03 +0000 Subject: [PATCH 5/5] simplify doc example --- docs/docs/integrations.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/docs/integrations.md b/docs/docs/integrations.md index 17a6e150..4374c0af 100644 --- a/docs/docs/integrations.md +++ b/docs/docs/integrations.md @@ -45,12 +45,8 @@ from typing import Annotated from cloudpathlib import S3Path from pydantic import BaseModel, PlainSerializer -def serialize_as_http(path: S3Path) -> str: - """Convert S3Path to HTTP URL""" - return f"https://{path.bucket}.s3.amazonaws.com/{path.key}" - class MyModel(BaseModel): - s3_file: Annotated[S3Path, PlainSerializer(serialize_as_http)] + s3_file: Annotated[S3Path, PlainSerializer(lambda x: x.as_url())] inst = MyModel(s3_file="s3://mybucket/myfile.txt") inst.model_dump_json()