diff --git a/msgspec/_core.c b/msgspec/_core.c index ccbda963..1bf21643 100644 --- a/msgspec/_core.c +++ b/msgspec/_core.c @@ -5675,7 +5675,15 @@ structmeta_process_default(StructMetaInfo *info, PyObject *name) { type = Py_TYPE(obj); } else if (f->default_factory != NODEFAULT) { - default_val = Factory_New(f->default_factory); + if (f->default_factory == (PyObject *)&PyTuple_Type) { + default_val = PyTuple_New(0); + } + else if (f->default_factory == (PyObject *)&PyFrozenSet_Type) { + default_val = PyFrozenSet_New(NULL); + } + else { + default_val = Factory_New(f->default_factory); + } if (default_val == NULL) return -1; goto done; } diff --git a/tests/test_common.py b/tests/test_common.py index 132e3dcd..c5bd92fa 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1744,6 +1744,22 @@ class Test(Struct, omit_defaults=True): res = proto.decode(proto.encode(obj)) assert res == sol + @pytest.mark.parametrize("typ", [tuple, list, set, frozenset, dict]) + def test_omit_defaults_collections(self, proto, typ): + """Check that using empty collections as default values are detected + regardless if they're specified by value or as a default_factory.""" + + class Test(Struct, omit_defaults=True): + a: typ = msgspec.field(default_factory=typ) + b: typ = msgspec.field(default=typ()) + c: typ = typ() + + ex = {"x": 1} if typ is dict else [1] + + assert proto.encode(Test()) == proto.encode({}) + for n in ["a", "b", "c"]: + assert proto.encode(Test(**{n: typ(ex)})) == proto.encode({n: ex}) + def test_omit_defaults_positional(self, proto): class Test(Struct, omit_defaults=True): a: int