Repository: strawberry-graphql/strawberry

Issue:
Interface values don't convert correctly from Pydantic models
When calling `from_pydantic` on a class with an interface field, the field value is always converted into an instance of the base class, no matter what its starting type is. The expected behavior should probably be to convert to the corresponding subtype class instead. See here for an example: https://gist.github.com/Matt343/fbce0cdffe1523bb22016bed6f65473f


Discussion:

Resolving patch:
diff --git a/strawberry/experimental/pydantic/__init__.py b/strawberry/experimental/pydantic/__init__.py
--- a/strawberry/experimental/pydantic/__init__.py
+++ b/strawberry/experimental/pydantic/__init__.py
@@ -1,6 +1,6 @@
 from .error_type import error_type
 from .exceptions import UnregisteredTypeException
-from .object_type import input, type
+from .object_type import input, interface, type
 
 
-__all__ = ["error_type", "UnregisteredTypeException", "input", "type"]
+__all__ = ["error_type", "UnregisteredTypeException", "input", "type", "interface"]
diff --git a/strawberry/experimental/pydantic/conversion.py b/strawberry/experimental/pydantic/conversion.py
--- a/strawberry/experimental/pydantic/conversion.py
+++ b/strawberry/experimental/pydantic/conversion.py
@@ -1,5 +1,6 @@
 from typing import Union, cast
 
+from strawberry.enum import EnumDefinition
 from strawberry.field import StrawberryField
 from strawberry.scalars import is_scalar
 from strawberry.type import StrawberryList, StrawberryOptional, StrawberryType
@@ -27,6 +28,8 @@ def _convert_from_pydantic_to_strawberry_type(
                 return _convert_from_pydantic_to_strawberry_type(
                     option_type, data_from_model=data, extra=extra
                 )
+    if isinstance(type_, EnumDefinition):
+        return data
     if isinstance(type_, StrawberryList):
         items = []
         for index, item in enumerate(data):
@@ -42,6 +45,10 @@ def _convert_from_pydantic_to_strawberry_type(
     elif is_scalar(type_):
         return data
     else:
+        # in the case of an interface, the concrete type may be more specific
+        # than the type in the field definition
+        if hasattr(type(data), "_strawberry_type"):
+            type_ = type(data)._strawberry_type
         return convert_pydantic_model_to_strawberry_class(
             type_, model_instance=data_from_model, extra=extra
         )
diff --git a/strawberry/experimental/pydantic/object_type.py b/strawberry/experimental/pydantic/object_type.py
--- a/strawberry/experimental/pydantic/object_type.py
+++ b/strawberry/experimental/pydantic/object_type.py
@@ -132,6 +132,7 @@ def wrap(cls):
         cls = dataclasses.make_dataclass(
             cls.__name__,
             sorted_fields,
+            bases=cls.__bases__,
         )
 
         _process_type(
@@ -165,3 +166,5 @@ def to_pydantic(self) -> Any:
 
 
 input = partial(type, is_input=True)
+
+interface = partial(type, is_interface=True)


Test patch:
diff --git a/tests/experimental/pydantic/schema/test_basic.py b/tests/experimental/pydantic/schema/test_basic.py
--- a/tests/experimental/pydantic/schema/test_basic.py
+++ b/tests/experimental/pydantic/schema/test_basic.py
@@ -1,5 +1,6 @@
 import textwrap
-from typing import List, Optional
+from enum import Enum
+from typing import List, Optional, Union
 
 import pydantic
 
@@ -233,3 +234,120 @@ def user(self) -> User:
     assert not result.errors
     assert result.data["user"]["age"] == 20
     assert result.data["user"]["ageInMonths"] == 240
+
+
+def test_basic_type_with_union():
+    class BranchA(pydantic.BaseModel):
+        field_a: str
+
+    class BranchB(pydantic.BaseModel):
+        field_b: int
+
+    class User(pydantic.BaseModel):
+        union_field: Union[BranchA, BranchB]
+
+    @strawberry.experimental.pydantic.type(BranchA, fields=["field_a"])
+    class BranchAType:
+        pass
+
+    @strawberry.experimental.pydantic.type(BranchB, fields=["field_b"])
+    class BranchBType:
+        pass
+
+    @strawberry.experimental.pydantic.type(User, fields=["age", "union_field"])
+    class UserType:
+        pass
+
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        def user(self) -> UserType:
+            return UserType(union_field=BranchBType(field_b=10))
+
+    schema = strawberry.Schema(query=Query)
+
+    query = "{ user { unionField { ... on BranchBType { fieldB } } } }"
+
+    result = schema.execute_sync(query)
+
+    assert not result.errors
+    assert result.data["user"]["unionField"]["fieldB"] == 10
+
+
+def test_basic_type_with_enum():
+    @strawberry.enum
+    class UserKind(Enum):
+        user = 0
+        admin = 1
+
+    class User(pydantic.BaseModel):
+        age: int
+        kind: UserKind
+
+    @strawberry.experimental.pydantic.type(User, fields=["age", "kind"])
+    class UserType:
+        pass
+
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        def user(self) -> UserType:
+            return UserType(age=10, kind=UserKind.admin)
+
+    schema = strawberry.Schema(query=Query)
+
+    query = "{ user { kind } }"
+
+    result = schema.execute_sync(query)
+
+    assert not result.errors
+    assert result.data["user"]["kind"] == "admin"
+
+
+def test_basic_type_with_interface():
+    class Base(pydantic.BaseModel):
+        base_field: str
+
+    class BranchA(Base):
+        field_a: str
+
+    class BranchB(Base):
+        field_b: int
+
+    class User(pydantic.BaseModel):
+        interface_field: Base
+
+    @strawberry.experimental.pydantic.interface(Base, fields=["base_field"])
+    class BaseType:
+        pass
+
+    @strawberry.experimental.pydantic.type(BranchA, fields=["field_a"])
+    class BranchAType(BaseType):
+        pass
+
+    @strawberry.experimental.pydantic.type(BranchB, fields=["field_b"])
+    class BranchBType(BaseType):
+        pass
+
+    @strawberry.experimental.pydantic.type(User, fields=["age", "interface_field"])
+    class UserType:
+        pass
+
+    print(BranchAType._type_definition)
+    print(BaseType._type_definition)
+
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        def user(self) -> UserType:
+            return UserType(interface_field=BranchBType(base_field="abc", field_b=10))
+
+    schema = strawberry.Schema(query=Query, types=[BranchAType, BranchBType])
+
+    query = "{ user { interfaceField { baseField, ... on BranchBType { fieldB } } } }"
+
+    result = schema.execute_sync(query)
+
+    assert not result.errors
+    assert result.data["user"]["interfaceField"]["baseField"] == "abc"
+    assert result.data["user"]["interfaceField"]["fieldB"] == 10
diff --git a/tests/experimental/pydantic/test_basic.py b/tests/experimental/pydantic/test_basic.py
--- a/tests/experimental/pydantic/test_basic.py
+++ b/tests/experimental/pydantic/test_basic.py
@@ -1,13 +1,16 @@
-from typing import List, Optional
+from enum import Enum
+from typing import List, Optional, Union
 
 import pytest
 
 import pydantic
 
 import strawberry
+from strawberry.enum import EnumDefinition
 from strawberry.experimental.pydantic.exceptions import MissingFieldsListError
 from strawberry.type import StrawberryList, StrawberryOptional
 from strawberry.types.types import TypeDefinition
+from strawberry.union import StrawberryUnion
 
 
 def test_basic_type():
@@ -250,3 +253,109 @@ class User:
     assert field2.python_name == "password"
     assert isinstance(field2.type, StrawberryOptional)
     assert field2.type.of_type is str
+
+
+def test_union():
+    class BranchA(pydantic.BaseModel):
+        field_a: str
+
+    class BranchB(pydantic.BaseModel):
+        field_b: int
+
+    class User(pydantic.BaseModel):
+        age: int
+        union_field: Union[BranchA, BranchB]
+
+    @strawberry.experimental.pydantic.type(BranchA, fields=["field_a"])
+    class BranchAType:
+        pass
+
+    @strawberry.experimental.pydantic.type(BranchB, fields=["field_b"])
+    class BranchBType:
+        pass
+
+    @strawberry.experimental.pydantic.type(User, fields=["age", "union_field"])
+    class UserType:
+        pass
+
+    definition: TypeDefinition = UserType._type_definition
+    assert definition.name == "UserType"
+
+    [field1, field2] = definition.fields
+
+    assert field1.python_name == "age"
+    assert field1.type is int
+
+    assert field2.python_name == "union_field"
+    assert isinstance(field2.type, StrawberryUnion)
+    assert field2.type.types[0] is BranchAType
+    assert field2.type.types[1] is BranchBType
+
+
+def test_enum():
+    @strawberry.enum
+    class UserKind(Enum):
+        user = 0
+        admin = 1
+
+    class User(pydantic.BaseModel):
+        age: int
+        kind: UserKind
+
+    @strawberry.experimental.pydantic.type(User, fields=["age", "kind"])
+    class UserType:
+        pass
+
+    definition: TypeDefinition = UserType._type_definition
+    assert definition.name == "UserType"
+
+    [field1, field2] = definition.fields
+
+    assert field1.python_name == "age"
+    assert field1.type is int
+
+    assert field2.python_name == "kind"
+    assert isinstance(field2.type, EnumDefinition)
+    assert field2.type.wrapped_cls is UserKind
+
+
+def test_interface():
+    class Base(pydantic.BaseModel):
+        base_field: str
+
+    class BranchA(Base):
+        field_a: str
+
+    class BranchB(Base):
+        field_b: int
+
+    class User(pydantic.BaseModel):
+        age: int
+        interface_field: Base
+
+    @strawberry.experimental.pydantic.interface(Base, fields=["base_field"])
+    class BaseType:
+        pass
+
+    @strawberry.experimental.pydantic.type(BranchA, fields=["field_a"])
+    class BranchAType(BaseType):
+        pass
+
+    @strawberry.experimental.pydantic.type(BranchB, fields=["field_b"])
+    class BranchBType(BaseType):
+        pass
+
+    @strawberry.experimental.pydantic.type(User, fields=["age", "interface_field"])
+    class UserType:
+        pass
+
+    definition: TypeDefinition = UserType._type_definition
+    assert definition.name == "UserType"
+
+    [field1, field2] = definition.fields
+
+    assert field1.python_name == "age"
+    assert field1.type is int
+
+    assert field2.python_name == "interface_field"
+    assert field2.type is BaseType
diff --git a/tests/experimental/pydantic/test_conversion.py b/tests/experimental/pydantic/test_conversion.py
--- a/tests/experimental/pydantic/test_conversion.py
+++ b/tests/experimental/pydantic/test_conversion.py
@@ -1,3 +1,4 @@
+from enum import Enum
 from typing import List, Optional, Union
 
 import pydantic
@@ -92,7 +93,7 @@ class WorkModel(pydantic.BaseModel):
         name: str
 
     @strawberry.experimental.pydantic.type(WorkModel, fields=["name"])
-    class Work(pydantic.BaseModel):
+    class Work:
         pass
 
     class UserModel(pydantic.BaseModel):
@@ -113,7 +114,7 @@ class WorkModel(pydantic.BaseModel):
         name: str
 
     @strawberry.experimental.pydantic.type(WorkModel, fields=["name"])
-    class Work(pydantic.BaseModel):
+    class Work:
         pass
 
     class UserModel(pydantic.BaseModel):
@@ -341,6 +342,72 @@ class UserType:
     assert user.union_field is None
 
 
+def test_can_convert_pydantic_type_to_strawberry_with_enum():
+    @strawberry.enum
+    class UserKind(Enum):
+        user = 0
+        admin = 1
+
+    class User(pydantic.BaseModel):
+        age: int
+        kind: UserKind
+
+    @strawberry.experimental.pydantic.type(User, fields=["age", "kind"])
+    class UserType:
+        pass
+
+    origin_user = User(age=1, kind=UserKind.user)
+    user = UserType.from_pydantic(origin_user)
+
+    assert user.age == 1
+    assert user.kind == UserKind.user
+
+
+def test_can_convert_pydantic_type_to_strawberry_with_interface():
+    class Base(pydantic.BaseModel):
+        base_field: str
+
+    class BranchA(Base):
+        field_a: str
+
+    class BranchB(Base):
+        field_b: int
+
+    class User(pydantic.BaseModel):
+        age: int
+        interface_field: Base
+
+    @strawberry.experimental.pydantic.interface(Base, fields=["base_field"])
+    class BaseType:
+        pass
+
+    @strawberry.experimental.pydantic.type(BranchA, fields=["field_a"])
+    class BranchAType(BaseType):
+        pass
+
+    @strawberry.experimental.pydantic.type(BranchB, fields=["field_b"])
+    class BranchBType(BaseType):
+        pass
+
+    @strawberry.experimental.pydantic.type(User, fields=["age", "interface_field"])
+    class UserType:
+        pass
+
+    origin_user = User(age=1, interface_field=BranchA(field_a="abc", base_field="def"))
+    user = UserType.from_pydantic(origin_user)
+
+    assert user.age == 1
+    assert isinstance(user.interface_field, BranchAType)
+    assert user.interface_field.field_a == "abc"
+
+    origin_user = User(age=1, interface_field=BranchB(field_b=123, base_field="def"))
+    user = UserType.from_pydantic(origin_user)
+
+    assert user.age == 1
+    assert isinstance(user.interface_field, BranchBType)
+    assert user.interface_field.field_b == 123
+
+
 def test_can_convert_pydantic_type_to_strawberry_with_additional_fields():
     class UserModel(pydantic.BaseModel):
         password: Optional[str]