@@ -88,6 +88,31 @@ def convert(self, ctx: Context) -> t.Any:
8888 ) and not isinstance (datatype , TupleTypeHint ):
8989 raise NotImplementedError
9090
91+ # NamedTuples with type information are a lot like data classes, so we delegate to the SchemaConverter.
92+ if (
93+ isinstance (datatype , ClassTypeHint )
94+ and issubclass (datatype .type , tuple )
95+ and getattr (datatype .type , "__annotations__" , None )
96+ ):
97+ schema = Schema (
98+ fields = {
99+ name : Field (
100+ datatype = TypeHint (type_ ),
101+ )
102+ for name , type_ in getattr (datatype .type , "__annotations__" ).items ()
103+ },
104+ constructor = datatype .type ,
105+ type = datatype .type ,
106+ )
107+ if ctx .direction .is_serialize ():
108+ return SchemaConverter ().serialize_from_schema (ctx , schema )
109+ elif ctx .direction .is_deserialize ():
110+ return SchemaConverter ().deserialize_from_schema (ctx , schema )
111+ else :
112+ assert False , ctx .direction
113+
114+ # TODO(@niklas.rosenstein): Should we support an object-based JSON representation for collections.namedtuple?
115+
91116 if isinstance (datatype , TupleTypeHint ) and not datatype .repeated :
92117 # Require that the length of the input data matches the tuple.
93118 item_types_iterator = iter (datatype )
@@ -129,6 +154,9 @@ def _length_check() -> None:
129154 values = list (values )
130155 if python_type == list :
131156 return values
157+ elif hasattr (python_type , "_fields" ): # For collections.namedtuple
158+ return python_type (* values )
159+
132160 try :
133161 return python_type (values )
134162 except TypeError :
@@ -431,9 +459,7 @@ def _get_schema(self, ctx: Context) -> Schema:
431459 except ValueError as exc :
432460 raise NotImplementedError (str (exc ))
433461
434- def serialize (self , ctx : Context ) -> t .MutableMapping [str , t .Any ]:
435- schema = self ._get_schema (ctx )
436-
462+ def serialize_from_schema (self , ctx : Context , schema : Schema ) -> t .MutableMapping [str , t .Any ]:
437463 try :
438464 is_instance = isinstance (ctx .value , schema .type )
439465 except TypeError :
@@ -502,9 +528,7 @@ def _get_field_value(field_name: str, field: Field) -> t.Any:
502528
503529 return result
504530
505- def deserialize (self , ctx : Context ) -> t .Any :
506- schema = self ._get_schema (ctx )
507-
531+ def deserialize_from_schema (self , ctx : Context , schema : Schema ) -> t .Any :
508532 if not isinstance (ctx .value , t .Mapping ):
509533 raise ConversionError .expected (self , ctx , t .Mapping )
510534
@@ -575,6 +599,14 @@ def _extract_fields(fields: t.Dict[str, Field]) -> t.Dict[str, t.Any]:
575599
576600 return schema .constructor (** result )
577601
602+ def deserialize (self , ctx : Context ) -> t .Any :
603+ schema = self ._get_schema (ctx )
604+ return self .deserialize_from_schema (ctx , schema )
605+
606+ def serialize (self , ctx : Context ) -> t .MutableMapping [str , t .Any ]:
607+ schema = self ._get_schema (ctx )
608+ return self .serialize_from_schema (ctx , schema )
609+
578610
579611class StringifyConverter (Converter ):
580612 """A useful helper converter that matches on a given type or its subclasses and converts them to a string for
0 commit comments