Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Instances of generic types with bound=BaseModel are not serialised #7562

Closed
1 task done
carlosds731 opened this issue Sep 22, 2023 · 2 comments · Fixed by #7606
Closed
1 task done

Instances of generic types with bound=BaseModel are not serialised #7562

carlosds731 opened this issue Sep 22, 2023 · 2 comments · Fixed by #7606
Assignees
Labels

Comments

@carlosds731
Copy link

carlosds731 commented Sep 22, 2023

Initial Checks

  • I confirm that I'm using Pydantic V2

Description

I have an Error class that optionally includes details, which should be a subclass of Pydantic's BaseModel. My aim is to specify the type of these details. Here's how I've implemented it:

ErrorDataT = TypeVar("ErrorDataT", bound=BaseModel)

class Error(BaseModel, Generic[ErrorDataT]):
    message: str
    details: Optional[ErrorDataT]

With this, the details field is strongly typed and ensures that, if present, it's always an instance of Pydantic's BaseModel. However, when I call error.model_dump(), the details field serializes as an empty dictionary. Removing the bound constraint on ErrorDataT allows the model to serialize correctly, but at the expense of type validation, i.e., I can assign any value to error.details.

In the tests provided below, both should ideally pass. But currently, only one of them does based on the bound constraint:

  • With the constraint:test_can_not_use_non_base_models_as_error_details passes, buttest_sample_error_is_correctly_serialized fails.
  • Without the constraint:test_sample_error_is_correctly_serialized passes, buttest_can_not_use_non_base_models_as_error_details fails.

NOTE: This was working in v1 (using GenericModel). I apologize in advance if I didn't understand something while doing the migration from v1 to v2.

Example Code

from pydantic import BaseModel, ValidationError
from typing import TypeVar, Generic, Optional
import pytest

ErrorDataT = TypeVar("ErrorDataT", bound=BaseModel)


class ErrorDetails(BaseModel):
    foo: str


class ErrorDetailsNonBaseModel:
    other: str

    def __init__(self, other) -> None:
        self.other = other


class Error(BaseModel, Generic[ErrorDataT]):
    message: str
    details: Optional[ErrorDataT]


sample_error = Error(
    message="We just had an error",
    details=ErrorDetails(foo="var"),
)


class TestApiErrorResponse:
    def test_sample_error_is_correctly_serialized(self):
        assert sample_error.model_dump() == {
            "message": "We just had an error",
            "details": {
                "foo": "var",
            },
        }

    def test_can_not_use_non_base_models_as_error_details(self):
        with pytest.raises(ValidationError):
            Error(
                message="We just had an error",
                details=ErrorDetailsNonBaseModel(other="some value"),
            )

Python, Pydantic & OS Version

pydantic version: 2.3.0
        pydantic-core version: 2.6.3
          pydantic-core build: profile=release pgo=false
                 install path: /Users/carlos/.virtualenvs/pydantic-issue/lib/python3.10/site-packages/pydantic
               python version: 3.10.8 (v3.10.8:aaaf517424, Oct 11 2022, 10:14:40) [Clang 13.0.0 (clang-1300.0.29.30)]
                     platform: macOS-13.5.1-arm64-arm-64bit
     optional deps. installed: ['typing-extensions']
@carlosds731 carlosds731 added bug V2 Bug related to Pydantic V2 unconfirmed Bug not yet confirmed as valid/applicable labels Sep 22, 2023
@sydney-runkle sydney-runkle self-assigned this Sep 25, 2023
@sydney-runkle sydney-runkle added question and removed unconfirmed Bug not yet confirmed as valid/applicable bug V2 Bug related to Pydantic V2 labels Sep 25, 2023
@sydney-runkle
Copy link
Member

Hi @carlosds731,

Thanks for your question! The current issue you're having is due to the fact that you haven't specified the generic parameter type when you instantiate the Error class.

If you do the following, your tests pass:

from pydantic import BaseModel, ValidationError
from typing import TypeVar, Generic, Optional
import pytest

ErrorDataT = TypeVar("ErrorDataT", bound=BaseModel)


class ErrorDetails(BaseModel):
    foo: str


class ErrorDetailsNonBaseModel:
    other: str

    def __init__(self, other) -> None:
        self.other = other


class Error(BaseModel, Generic[ErrorDataT]):
    message: str
    details: Optional[ErrorDataT]


sample_error = Error[ErrorDetails](
    message="We just had an error",
    details=ErrorDetails(foo="var"),
)

The reason for this is that we default to serializing with the bound type of a TypeVar if the type being used for a generic isn't specified.

Another approach that you could use is this:

from pydantic import BaseModel, SerializeAsAny
from typing import TypeVar, Generic, OptionalErrorDataT = TypeVar("ErrorDataT", bound=BaseModel)
​
​
class ErrorDetails(BaseModel):
    foo: str
​
​
class Error(BaseModel, Generic[ErrorDataT]):
    message: str
    details: Optional[SerializeAsAny[ErrorDataT]]
​
​
sample_error = Error(
    message="We just had an error",
    details=ErrorDetails(foo="var"),
)

Using the SerializeAsAny flag makes it such that whatever type ErrorDataT takes on, it's serialized as an Any type, but validated still against the ErrorDataT type schema.

All of this being said, we think that this behavior is confusing, so we've opened the PR mentioned above to fix this issue (so the original code you have should work, once that change is released).

@carlosds731
Copy link
Author

thank you very much @sydney-runkle

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants