diff --git a/msgspec/_core.c b/msgspec/_core.c index 147ad95a..f70d213d 100644 --- a/msgspec/_core.c +++ b/msgspec/_core.c @@ -8342,6 +8342,9 @@ TypedDictInfo_error_missing(TypedDictInfo *self, PyObject *dict, PathNode *path) } } } + // Should be unreachable, but may happen if the TypedDict info + // is inconsistent. + assert(0); } static int diff --git a/msgspec/_utils.py b/msgspec/_utils.py index 6d338104..ca7815ff 100644 --- a/msgspec/_utils.py +++ b/msgspec/_utils.py @@ -227,6 +227,13 @@ def get_typeddict_info(obj): hints[k] = v.__args__[0] else: hints[k] = v + + # This can happen if there is a bug in the TypedDict implementation; + # such a bug was present in Python 3.14. + if not all(k in hints for k in required): + raise RuntimeError( + f"Required set {required} contains keys that are no in hints: {hints.keys()}" + ) return hints, required diff --git a/tests/test_common.py b/tests/test_common.py index 38898be3..30f47be0 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -2112,6 +2112,24 @@ class Ex(Base, total=False): dec.decode(proto.encode({"b": "two"})) assert "Object missing required field `a`" == str(rec.value) + @pytest.mark.parametrize("use_typing_extensions", [False, True]) + def test_broken_typeddict(self, proto, use_typing_extensions): + # Check that we don't crash if a TypedDict has incorrect + # introspection data. + if use_typing_extensions: + tex = pytest.importorskip("typing_extensions") + cls = tex.TypedDict + else: + cls = TypedDict + + class Ex(cls, total=False): + c: str + Ex.__annotations__ = {"c": "str"} + Ex.__required_keys__ = {"a", "b"} + + with pytest.raises(RuntimeError): + proto.Decoder(Ex) + @pytest.mark.parametrize("use_typing_extensions", [False, True]) def test_required_and_notrequired(self, proto, use_typing_extensions): if use_typing_extensions: