Repository: oxan/djangorestframework-dataclasses

Issue:
DataclassSerializer fails with `source="*"` embedded serialiser fields because it requires Meta.dataclass
According to https://www.django-rest-framework.org/api-guide/fields/#using-source, the serialisers should allow for use of `source="*"`.

The issue can be reproduced in the following way. Copy the following snippet on the bottom of `tests/test_functional.py`:

```
class PetEmbeddedDefaultSerializer(DataclassSerializer):
    class PetInformationSerializer(DataclassSerializer):
        animal = fields.CharField()

    class Meta:
        dataclass = Pet
        fields = ['name', 'information']

    name = fields.CharField()
    information = PetInformationSerializer(source="*")


class SourceStartTest(TestCase):
    DATA = {'name': 'Milo', 'information': {'animal': 'cat'}}
    INSTANCE = Pet(name='Milo', animal='cat')

    def test_default_serialization(self):
        serializer = PetEmbeddedDefaultSerializer(instance=self.INSTANCE)
        self.assertDictEqual(serializer.data, self.DATA)

    def test_default_deserialization(self):
        serializer = PetEmbeddedDefaultSerializer(data=self.DATA)
        serializer.is_valid(raise_exception=True)
        self.assertEqual(serializer.instance, self.INSTANCE)
```

Discussion:

Patch:
diff --git a/rest_framework_dataclasses/serializers.py b/rest_framework_dataclasses/serializers.py
index d01d319..72a35e6 100644
--- a/rest_framework_dataclasses/serializers.py
+++ b/rest_framework_dataclasses/serializers.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import collections
 import copy
 import dataclasses
 import datetime
@@ -665,6 +666,23 @@ class DataclassSerializer(rest_framework.serializers.Serializer, Generic[T]):
         """
         native_values = super(DataclassSerializer, self).to_internal_value(data)
 
+        # If the source is a wildcard, don't try to instantiate anything, but just return the native values as a dict.
+        # It'll be merged into the native values of the parent serializer by the parent class.
+        if self.source == '*':
+            return native_values
+
+        # If any fields have a wildcard source, their native value will hold a values dictionary instead of a dataclass,
+        # so convert them into a dataclass.
+        if any(field.source == '*' for field in self.fields.values()):
+            for field_name, tp in self.dataclass_definition.field_types.items():
+                if (
+                    field_name in native_values and
+                    dataclasses.is_dataclass(tp) and
+                    isinstance(native_values[field_name], collections.abc.Mapping)
+                ):
+                    dataclass_fields = {field.name: field for field in dataclasses.fields(tp)}
+                    native_values[field_name] = _create_instance(tp, dataclass_fields, native_values[field_name])
+
         # Only insert empty sentinel value for non-supplied values when the root serializer is in partial mode, to
         # prevent them from showing up otherwise.
         if self.root.partial:


Test patch:
diff --git a/tests/test_functional.py b/tests/test_functional.py
index 34c3683..11e0167 100644
--- a/tests/test_functional.py
+++ b/tests/test_functional.py
@@ -9,6 +9,7 @@ import uuid
 from unittest import TestCase
 
 from rest_framework import fields
+from rest_framework.serializers import Serializer
 
 from rest_framework_dataclasses.serializers import DataclassSerializer
 from rest_framework_dataclasses.types import Literal
@@ -268,3 +269,59 @@ class ObscureFeaturesTest(TestCase, FunctionalTestMixin):
 
     def setUp(self):
         self.instance.name = 'Bob'
+
+# Testcase 5 (source='*')
+
+
+@dataclasses.dataclass
+class PersonPet:
+    name: str
+    pet: Pet
+
+
+class PersonPetEmbeddedDataclassSerializer(DataclassSerializer):
+    class PetSerializer(DataclassSerializer):
+        class Meta:
+            dataclass = Pet
+            fields = ['name', 'owner_name', 'animal']
+
+        name = fields.CharField(source='pet.name')
+        animal = fields.CharField(source='pet.animal')
+        owner_name = fields.CharField(source='name')
+
+    class Meta:
+        dataclass = PersonPet
+        fields = ['name', 'pet']
+
+    name = fields.CharField()
+    pet = PetSerializer(source='*')
+
+
+class PersonPetEmbeddedDefaultSerializer(DataclassSerializer):
+    class PetSerializer(Serializer):
+        class Meta:
+            dataclass = Pet
+            fields = ['name', 'owner_name', 'animal']
+
+        name = fields.CharField(source='pet.name')
+        animal = fields.CharField(source='pet.animal')
+        owner_name = fields.CharField(source='name')
+
+    class Meta:
+        dataclass = PersonPet
+        fields = ['name', 'pet']
+
+    name = fields.CharField()
+    pet = PetSerializer(source='*')
+
+
+class SourceStarDataclassTest(TestCase, FunctionalTestMixin):
+    serializer = PersonPetEmbeddedDataclassSerializer
+    instance = PersonPet(name='Milo', pet=Pet(name='Katsu', animal='cat'))
+    representation = {'name': 'Milo', 'pet': {'name': 'Katsu', 'animal': 'cat', 'owner_name': 'Milo'}}
+
+
+class SourceStarDefaultTest(TestCase, FunctionalTestMixin):
+    serializer = PersonPetEmbeddedDefaultSerializer
+    instance = PersonPet(name='Milo', pet=Pet(name='Katsu', animal='cat'))
+    representation = {'name': 'Milo', 'pet': {'name': 'Katsu', 'animal': 'cat', 'owner_name': 'Milo'}}