Repository: jcrist/msgspec (language: python)

Issue:
Json Encoder: dataclass classes are encoded to empty dict
### Question

I've noticed that when encoding a dataclass class (as opposed to an instance), the class is encoded to an empty dict.

I was wondering if this was intentional behaviour? I would have expected an error as if I had passed in any other class (rather than instance).

```python
>>> import msgspec.json
>>> e = msgspec.json.Encoder()
>>> from dataclasses import dataclass
>>> @dataclass
... class SomeDataclass:
...   foo: int
... 
>>> e.encode(SomeDataclass)
b'{}'
>>> class SomeClass:
...  pass
... 
>>> e.encode(SomeClass)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Encoding objects of type type is unsupported
```


Discussion / hints:

Fix patch:
diff --git a/msgspec/_core.c b/msgspec/_core.c
index d2087b85..949f8a8f 100644
--- a/msgspec/_core.c
+++ b/msgspec/_core.c
@@ -13137,7 +13137,7 @@ mpack_encode_uncommon(EncoderState *self, PyTypeObject *type, PyObject *obj)
     else if (PyAnySet_Check(obj)) {
         return mpack_encode_set(self, obj);
     }
-    else if (type->tp_dict != NULL) {
+    else if (!PyType_Check(obj) && type->tp_dict != NULL) {
         PyObject *fields = PyObject_GetAttr(obj, self->mod->str___dataclass_fields__);
         if (fields != NULL) {
             int status = mpack_encode_dataclass(self, obj, fields);
@@ -14231,7 +14231,7 @@ json_encode_uncommon(EncoderState *self, PyTypeObject *type, PyObject *obj) {
     else if (PyAnySet_Check(obj)) {
         return json_encode_set(self, obj);
     }
-    else if (type->tp_dict != NULL) {
+    else if (!PyType_Check(obj) && type->tp_dict != NULL) {
         PyObject *fields = PyObject_GetAttr(obj, self->mod->str___dataclass_fields__);
         if (fields != NULL) {
             int status = json_encode_dataclass(self, obj, fields);
@@ -19924,7 +19924,7 @@ to_builtins(ToBuiltinsState *self, PyObject *obj, bool is_key) {
     else if (PyAnySet_Check(obj)) {
         return to_builtins_set(self, obj, is_key);
     }
-    else if (type->tp_dict != NULL) {
+    else if (!PyType_Check(obj) && type->tp_dict != NULL) {
         PyObject *fields = PyObject_GetAttr(obj, self->mod->str___dataclass_fields__);
         if (fields != NULL) {
             PyObject *out = to_builtins_dataclass(self, obj, fields);


Test patch:
diff --git a/tests/test_common.py b/tests/test_common.py
index c5bd92fa..2ef8aaf2 100644
--- a/tests/test_common.py
+++ b/tests/test_common.py
@@ -2490,6 +2490,14 @@ class Ex:
         with pytest.raises(RuntimeError, match="is not a dict"):
             proto.encode(Ex(1))
 
+    def test_encode_dataclass_class_errors(self, proto):
+        @dataclass
+        class Ex:
+            x: int
+
+        with pytest.raises(TypeError, match="Encoding objects of type type"):
+            proto.encode(Ex)
+
     def test_encode_dataclass_no_slots(self, proto):
         @dataclass
         class Test:
diff --git a/tests/test_to_builtins.py b/tests/test_to_builtins.py
index 0f726e8b..56da76e9 100644
--- a/tests/test_to_builtins.py
+++ b/tests/test_to_builtins.py
@@ -428,6 +428,14 @@ class Ex:
         with pytest.raises(TypeError, match="Encoding objects of type Bad"):
             to_builtins(msg)
 
+    def test_dataclass_class_errors(self):
+        @dataclass
+        class Ex:
+            x: int
+
+        with pytest.raises(TypeError, match="Encoding objects of type type"):
+            to_builtins(Ex)
+
     @pytest.mark.parametrize("slots", [True, False])
     def test_attrs(self, slots):
         attrs = pytest.importorskip("attrs")