Repository: pydantic/pydantic

Issue:
support for built-in `dataclass` breaks pickle
### Checks

* [x] I added a descriptive title to this issue
* [x] I have searched (google, github) for similar issues and couldn't find anything
* [x] I have read and followed [the docs](https://pydantic-docs.helpmanual.io/) and still think this is a bug

<!-- Sorry to sound so draconian, but every second saved replying to issues is time spend improving pydantic :-) -->

# Bug

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.7.2
            pydantic compiled: True
                 install path: /Users/aimestereo/.pyenv/versions/3.7.6/envs/feed/lib/python3.7/site-packages/pydantic
               python version: 3.7.6 (default, Jan 30 2020, 16:07:03)  [Clang 11.0.0 (clang-1100.0.33.17)]
                     platform: Darwin-19.6.0-x86_64-i386-64bit
     optional deps. installed: ['typing-extensions']
```
<!-- or if you're using pydantic prior to v1.3, manually include: OS, python version and pydantic version -->

<!-- Please read the [docs](https://pydantic-docs.helpmanual.io/) and search through issues to
confirm your bug hasn't already been reported. -->

<!-- Where possible please include a self-contained code snippet describing your bug: -->

thanks to @PrettyWood 1.7 Introduced cool [support of built-in dataclass](https://github.com/samuelcolvin/pydantic/pull/1817). But in our project this broke pickle

```py
import pickle
import dataclasses

import pydantic


@dataclasses.dataclass
class BuiltInDataclass:
    value: int


class PydanticModel(pydantic.BaseModel):
    built_in_dataclass: BuiltInDataclass


print(pickle.dumps(
    PydanticModel(built_in_dataclass=BuiltInDataclass(value=0))
))
```

output:
```
---------------------------------------------------------------------------
PicklingError                             Traceback (most recent call last)
<ipython-input-14-7d67a44cf353> in <module>
     15
     16 print(pickle.dumps(
---> 17     PydanticModel(built_in_dataclass=BuiltInDataclass(value=0))
     18 ))

PicklingError: Can't pickle <class '__main__.BuiltInDataclass'>: it's not the same object as __main__.BuiltInDataclass
```

---

For better understanding here's an issue of pickle outside of pydantic context. This is an old pickle problem, when different classes have the same name, simple way to reproduce:
```
import pickle


class A: pass


# this is an analogy of how pydantic convert built-in dataclass
B = type(A.__name__, (A,), {})


print(A)
# <class '__main__.A'>

print(B)
# <class '__main__.A'>

pickle.dumps(A)
# b'\x80\x03c__main__\nA\nq\x00.'

pickle.dumps(B)
# error
```
output
```
---------------------------------------------------------------------------
PicklingError                             Traceback (most recent call last)
<ipython-input-2-b97d2c67736c> in <module>
     10
     11 print(pickle.dumps(A))
---> 12 print(pickle.dumps(B))

PicklingError: Can't pickle <class '__main__.A'>: it's not the same object as __main__.A
```

---

As a result we can't use this feature, we're stick to intermediate classes that converts buit-in dataclasses to pydantic:

```
class SerializedDataclass(pydantic.BaseModel):
	# copy/paste of all fields from built-in dataclass
    value: float

```


Discussion:

Resolving patch:
diff --git a/pydantic/dataclasses.py b/pydantic/dataclasses.py
--- a/pydantic/dataclasses.py
+++ b/pydantic/dataclasses.py
@@ -119,10 +119,23 @@ def _pydantic_post_init(self: 'Dataclass', *initvars: Any) -> None:
     #   __post_init__ = _pydantic_post_init
     # ```
     # with the exact same fields as the base dataclass
+    # and register it on module level to address pickle problem:
+    # https://github.com/samuelcolvin/pydantic/issues/2111
     if is_builtin_dataclass(_cls):
+        uniq_class_name = f'_Pydantic_{_cls.__name__}_{id(_cls)}'
         _cls = type(
-            _cls.__name__, (_cls,), {'__annotations__': _cls.__annotations__, '__post_init__': _pydantic_post_init}
+            # for pretty output new class will have the name as original
+            _cls.__name__,
+            (_cls,),
+            {
+                '__annotations__': _cls.__annotations__,
+                '__post_init__': _pydantic_post_init,
+                # attrs for pickle to find this class
+                '__module__': __name__,
+                '__qualname__': uniq_class_name,
+            },
         )
+        globals()[uniq_class_name] = _cls
     else:
         _cls.__post_init__ = _pydantic_post_init
     cls: Type['Dataclass'] = dataclasses.dataclass(  # type: ignore


Test patch:
diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py
--- a/tests/test_dataclasses.py
+++ b/tests/test_dataclasses.py
@@ -1,4 +1,5 @@
 import dataclasses
+import pickle
 from collections.abc import Hashable
 from datetime import datetime
 from pathlib import Path
@@ -733,7 +734,10 @@ class File:
                 'type': 'object',
             }
         },
-        'properties': {'filename': {'title': 'Filename', 'type': 'string'}, 'meta': {'$ref': '#/definitions/Meta'}},
+        'properties': {
+            'filename': {'title': 'Filename', 'type': 'string'},
+            'meta': {'$ref': '#/definitions/Meta'},
+        },
         'required': ['filename', 'meta'],
         'title': 'File',
         'type': 'object',
@@ -795,3 +799,37 @@ class Config:
     e.other = 'bulbi2'
     with pytest.raises(dataclasses.FrozenInstanceError):
         e.item.name = 'pika2'
+
+
+def test_pickle_overriden_builtin_dataclass(create_module):
+    module = create_module(
+        # language=Python
+        """\
+import dataclasses
+import pydantic
+
+
+@dataclasses.dataclass
+class BuiltInDataclassForPickle:
+    value: int
+
+class ModelForPickle(pydantic.BaseModel):
+    # pickle can only work with top level classes as it imports them
+
+    dataclass: BuiltInDataclassForPickle
+
+    class Config:
+        validate_assignment = True
+        """
+    )
+    obj = module.ModelForPickle(dataclass=module.BuiltInDataclassForPickle(value=5))
+
+    pickled_obj = pickle.dumps(obj)
+    restored_obj = pickle.loads(pickled_obj)
+
+    assert restored_obj.dataclass.value == 5
+    assert restored_obj == obj
+
+    # ensure the restored dataclass is still a pydantic dataclass
+    with pytest.raises(ValidationError, match='value\n +value is not a valid integer'):
+        restored_obj.dataclass.value = 'value of a wrong type'