Edit on GitHub

serde.se

This module provides serialize, is_serializable to_dict, to_tuple and classes and functions associated with serialization.

   1"""
   2This module provides `serialize`, `is_serializable` `to_dict`, `to_tuple` and classes
   3and functions associated with serialization.
   4"""
   5
   6from __future__ import annotations
   7import abc
   8import copy
   9import dataclasses
  10import functools
  11import typing
  12import itertools
  13import jinja2
  14from dataclasses import dataclass, is_dataclass
  15from typing import Any, Generic, Literal, TypeVar
  16from collections import deque, Counter
  17from collections.abc import (
  18    Callable,
  19    Iterable,
  20    Iterator,
  21    Mapping,
  22    Set,
  23)
  24
  25from beartype import beartype, BeartypeConf
  26from beartype.door import is_bearable
  27from typing_extensions import dataclass_transform
  28
  29from .compat import (
  30    SerdeError,
  31    SerdeSkip,
  32    T,
  33    get_origin,
  34    is_any,
  35    is_bare_counter,
  36    is_bare_deque,
  37    is_bare_dict,
  38    is_bare_list,
  39    is_bare_opt,
  40    is_bare_set,
  41    is_bare_tuple,
  42    is_class_var,
  43    is_counter,
  44    is_datetime,
  45    is_datetime_instance,
  46    is_deque,
  47    is_dict,
  48    is_enum,
  49    is_flatten_dict,
  50    is_generic,
  51    is_list,
  52    is_literal,
  53    is_none,
  54    is_opt,
  55    is_primitive,
  56    is_set,
  57    is_str_serializable,
  58    is_str_serializable_instance,
  59    is_tuple,
  60    is_union,
  61    is_variable_tuple,
  62    is_pep695_type_alias,
  63    iter_types,
  64    iter_unions,
  65    type_args,
  66    typename,
  67)
  68from .core import (
  69    ClassSerializer,
  70    CACHE,
  71    SERDE_SCOPE,
  72    TO_DICT,
  73    TO_ITER,
  74    UNION_SE_PREFIX,
  75    DefaultTagging,
  76    Field,
  77    Scope,
  78    Tagging,
  79    TypeCheck,
  80    add_func,
  81    coerce_object,
  82    disabled,
  83    get_transparent_field,
  84    strict,
  85    fields,
  86    is_instance,
  87    logger,
  88    union_func_name,
  89    GLOBAL_CLASS_SERIALIZER,
  90)
  91
  92# Lazy numpy imports to improve startup time
  93
  94__all__ = ["serialize", "is_serializable", "to_dict", "to_tuple"]
  95
  96
  97# Lazy numpy import wrappers to improve startup time
  98def _is_numpy_array(typ: Any) -> bool:
  99    from .numpy import is_numpy_array
 100
 101    return is_numpy_array(typ)
 102
 103
 104def _is_numpy_scalar(typ: Any) -> bool:
 105    from .numpy import is_numpy_scalar
 106
 107    return is_numpy_scalar(typ)
 108
 109
 110def _is_numpy_jaxtyping(typ: Any) -> bool:
 111    from .numpy import is_numpy_jaxtyping
 112
 113    return is_numpy_jaxtyping(typ)
 114
 115
 116def _is_numpy_datetime(typ: Any) -> bool:
 117    from .numpy import is_numpy_datetime
 118
 119    return is_numpy_datetime(typ)
 120
 121
 122SerializeFunc = Callable[[type[Any], Any], Any]
 123""" Interface of Custom serialize function. """
 124
 125
 126def default_serializer(_cls: type[Any], obj: Any) -> Any:
 127    """
 128    Marker function to tell serde to use the default serializer. It's used when custom serializer
 129    is specified at the class but you want to override a field with the default serializer.
 130    """
 131
 132
 133def serde_legacy_custom_class_serializer(
 134    cls: type[Any], obj: Any, custom: SerializeFunc, default: Callable[[], Any]
 135) -> Any:
 136    try:
 137        return custom(cls, obj)
 138    except SerdeSkip:
 139        return default()
 140
 141
 142class Serializer(Generic[T], metaclass=abc.ABCMeta):
 143    """
 144    `Serializer` base class. Subclass this to customize serialize behaviour.
 145
 146    See `serde.json.JsonSerializer` and `serde.msgpack.MsgPackSerializer` for example usage.
 147    """
 148
 149    @classmethod
 150    @abc.abstractmethod
 151    def serialize(cls, obj: Any, **opts: Any) -> T:
 152        raise NotImplementedError
 153
 154
 155def _make_serialize(
 156    cls_name: str,
 157    fields: Iterable[str | tuple[str, type[Any]] | tuple[str, type[Any], Any]],
 158    *args: Any,
 159    rename_all: str | None = None,
 160    reuse_instances_default: bool = False,
 161    convert_sets_default: bool = False,
 162    serializer: SerializeFunc | None = None,
 163    tagging: Tagging = DefaultTagging,
 164    type_check: TypeCheck = disabled,
 165    serialize_class_var: bool = False,
 166    transparent: bool = False,
 167    class_serializer: ClassSerializer | None = None,
 168    **kwargs: Any,
 169) -> type[Any]:
 170    """
 171    Create a serializable class programatically.
 172    """
 173    C = dataclasses.make_dataclass(cls_name, fields, *args, **kwargs)
 174    C = serialize(
 175        C,
 176        rename_all=rename_all,
 177        reuse_instances_default=reuse_instances_default,
 178        convert_sets_default=convert_sets_default,
 179        serializer=serializer,
 180        tagging=tagging,
 181        type_check=type_check,
 182        serialize_class_var=serialize_class_var,
 183        transparent=transparent,
 184        **kwargs,
 185    )
 186    return C
 187
 188
 189# The `serialize` function can call itself recursively when it needs to generate code for
 190# unmarked dataclasses. To avoid infinite recursion, this array remembers types for which code is
 191# currently being generated.
 192GENERATION_STACK = []
 193
 194
 195@dataclass_transform()
 196def serialize(
 197    _cls: type[T] | None = None,
 198    rename_all: str | None = None,
 199    reuse_instances_default: bool = False,
 200    convert_sets_default: bool = False,
 201    serializer: SerializeFunc | None = None,
 202    tagging: Tagging = DefaultTagging,
 203    type_check: TypeCheck = strict,
 204    serialize_class_var: bool = False,
 205    transparent: bool = False,
 206    class_serializer: ClassSerializer | None = None,
 207    **kwargs: Any,
 208) -> type[T]:
 209    """
 210    A dataclass with this decorator is serializable into any of the data formats
 211    supported by pyserde.
 212
 213    >>> from datetime import datetime
 214    >>> from serde import serialize
 215    >>> from serde.json import to_json
 216    >>>
 217    >>> @serialize
 218    ... class Foo:
 219    ...     i: int
 220    ...     s: str
 221    ...     f: float
 222    ...     b: bool
 223    >>>
 224    >>> to_json(Foo(i=10, s='foo', f=100.0, b=True))
 225    '{"i":10,"s":"foo","f":100.0,"b":true}'
 226    """
 227
 228    def wrap(cls: type[T]) -> type[T]:
 229        tagging.check()
 230
 231        # If no `dataclass` found in the class, dataclassify it automatically.
 232        if not is_dataclass(cls):
 233            dataclass(cls)
 234
 235        if transparent:
 236            get_transparent_field(cls)
 237
 238        if type_check.is_strict():
 239            serde_beartype = beartype(conf=BeartypeConf(violation_type=SerdeError))
 240            serde_beartype(cls)
 241
 242        g: dict[str, Any] = {}
 243
 244        # Create a scope storage used by serde.
 245        # Each class should get own scope. Child classes can not share scope with parent class.
 246        # That's why we need the "scope.cls is not cls" check.
 247        scope: Scope | None = getattr(cls, SERDE_SCOPE, None)
 248        if scope is None or scope.cls is not cls:
 249            scope = Scope(
 250                cls,
 251                reuse_instances_default=reuse_instances_default,
 252                convert_sets_default=convert_sets_default,
 253            )
 254            setattr(cls, SERDE_SCOPE, scope)
 255        scope.transparent = transparent
 256
 257        class_serializers: list[ClassSerializer] = list(
 258            itertools.chain(GLOBAL_CLASS_SERIALIZER, [class_serializer] if class_serializer else [])
 259        )
 260
 261        # Set some globals for all generated functions
 262        g["cls"] = cls
 263        g["copy"] = copy
 264        g["serde_scope"] = scope
 265        g["SerdeError"] = SerdeError
 266        g["enum_value"] = enum_value
 267        g["is_dataclass"] = is_dataclass
 268        g["typename"] = typename  # used in union functions
 269        g["is_instance"] = is_instance  # used in union functions
 270        g["to_obj"] = to_obj
 271        g["typing"] = typing
 272        g["Literal"] = Literal
 273        g["TypeCheck"] = TypeCheck
 274        g["disabled"] = disabled
 275        g["coerce_object"] = coerce_object
 276        g["class_serializers"] = class_serializers
 277        if serializer:
 278            g["serde_legacy_custom_class_serializer"] = functools.partial(
 279                serde_legacy_custom_class_serializer, custom=serializer
 280            )
 281
 282        # Collect types used in the generated code.
 283        for typ in iter_types(cls):
 284            # When we encounter a dataclass not marked with serialize, then also generate serialize
 285            # functions for it.
 286            if is_dataclass_without_se(typ) and typ is not cls:
 287                # We call serialize and not wrap to make sure that we will use the default serde
 288                # configuration for generating the serialization function.
 289                serialize(typ)
 290
 291            if is_primitive(typ) and not is_enum(typ):
 292                continue
 293            g[typename(typ)] = typ
 294
 295        # render all union functions
 296        for union in iter_unions(cls):
 297            union_args = list(type_args(union))
 298            union_key = union_func_name(UNION_SE_PREFIX, union_args)
 299            add_func(scope, union_key, render_union_func(cls, union_args, tagging), g)
 300            scope.union_se_args[union_key] = union_args
 301
 302        for f in sefields(cls, serialize_class_var):
 303            if f.skip_if:
 304                g[f.skip_if.name] = f.skip_if
 305            if f.serializer:
 306                g[f.serializer.name] = f.serializer
 307
 308        add_func(
 309            scope,
 310            TO_ITER,
 311            render_to_tuple(cls, serializer, type_check, serialize_class_var, class_serializer),
 312            g,
 313        )
 314        add_func(
 315            scope,
 316            TO_DICT,
 317            render_to_dict(
 318                cls, rename_all, serializer, type_check, serialize_class_var, class_serializer
 319            ),
 320            g,
 321        )
 322
 323        logger.debug(f"{typename(cls)}: {SERDE_SCOPE} {scope}")
 324
 325        return cls
 326
 327    if _cls is None:
 328        return wrap  # type: ignore
 329
 330    if _cls in GENERATION_STACK:
 331        return _cls
 332
 333    GENERATION_STACK.append(_cls)
 334    try:
 335        return wrap(_cls)
 336    finally:
 337        GENERATION_STACK.pop()
 338
 339
 340def is_serializable(instance_or_class: Any) -> bool:
 341    """
 342    Test if an instance or class is serializable.
 343
 344    >>> @serialize
 345    ... class Foo:
 346    ...     pass
 347
 348    Testing `Foo` class object returns `True`.
 349    >>> is_serializable(Foo)
 350    True
 351
 352    Testing `Foo` object also returns `True`.
 353    >>> is_serializable(Foo())
 354    True
 355    """
 356    return hasattr(instance_or_class, SERDE_SCOPE)
 357
 358
 359def is_dataclass_without_se(cls: type[Any]) -> bool:
 360    if not dataclasses.is_dataclass(cls):
 361        return False
 362    if not hasattr(cls, SERDE_SCOPE):
 363        return True
 364    scope: Scope | None = getattr(cls, SERDE_SCOPE)
 365    if not scope:
 366        return True
 367    return TO_DICT not in scope.funcs
 368
 369
 370def to_obj(
 371    o: Any,
 372    named: bool,
 373    reuse_instances: bool | None = None,
 374    convert_sets: bool | None = None,
 375    skip_none: bool = False,
 376    c: Any | None = None,
 377) -> Any:
 378    def serializable_to_obj(object: Any) -> Any:
 379        serde_scope: Scope = getattr(object, SERDE_SCOPE)
 380        func_name = TO_DICT if named else TO_ITER
 381        return serde_scope.funcs[func_name](
 382            object,
 383            reuse_instances=reuse_instances,
 384            convert_sets=convert_sets,
 385            skip_none=skip_none,
 386        )
 387
 388    try:
 389        thisfunc = functools.partial(
 390            to_obj,
 391            named=named,
 392            reuse_instances=reuse_instances,
 393            convert_sets=convert_sets,
 394            skip_none=skip_none,
 395        )
 396
 397        # If a class in the argument is a non-dataclass class e.g. Union[Foo, Bar],
 398        # pyserde generates a wrapper (de)serializable dataclass on the fly,
 399        # and use it to serialize the object.
 400        if c and is_union(c) and not is_opt(c):
 401            return CACHE.serialize_union(c, o)
 402
 403        if o is None:
 404            return None
 405        if is_dataclass_without_se(o):
 406            # Do not automatically implement beartype if dataclass without serde decorator
 407            # is passed, because it is surprising for users
 408            # See https://github.com/yukinarit/pyserde/issues/480
 409            serialize(type(o), type_check=disabled)
 410            return serializable_to_obj(o)
 411        elif is_serializable(o):
 412            return serializable_to_obj(o)
 413        elif is_bearable(o, list):  # type: ignore[arg-type]  # pyright: ignore[reportArgumentType]
 414            return [thisfunc(e) for e in o]
 415        elif is_bearable(o, tuple):  # type: ignore[arg-type]  # pyright: ignore[reportArgumentType]
 416            return tuple(thisfunc(e) for e in o)
 417        elif isinstance(o, Mapping):
 418            return {k: thisfunc(v) for k, v in o.items()}
 419        elif isinstance(o, Set):
 420            return [thisfunc(e) for e in o]
 421        elif isinstance(o, deque):
 422            return [thisfunc(e) for e in o]
 423        elif isinstance(o, Counter):
 424            return dict(o)
 425        elif is_str_serializable_instance(o) or is_datetime_instance(o):
 426            se_cls = o.__class__ if not c or c is Any else c
 427            return CACHE.serialize(
 428                se_cls,
 429                o,
 430                reuse_instances=reuse_instances,
 431                convert_sets=convert_sets,
 432                skip_none=skip_none,
 433            )
 434
 435        return o
 436
 437    except Exception as e:
 438        raise SerdeError(e) from None
 439
 440
 441def astuple(v: Any) -> tuple[Any, ...]:
 442    """
 443    Serialize object into tuple.
 444    """
 445    return to_tuple(v, reuse_instances=False, convert_sets=False)
 446
 447
 448def to_tuple(
 449    o: Any,
 450    c: type[Any] | None = None,
 451    reuse_instances: bool | None = None,
 452    convert_sets: bool | None = None,
 453    skip_none: bool = False,
 454) -> tuple[Any, ...]:
 455    """
 456    Serialize object into tuple.
 457
 458    >>> @serialize
 459    ... class Foo:
 460    ...     i: int
 461    ...     s: str = 'foo'
 462    ...     f: float = 100.0
 463    ...     b: bool = True
 464    >>>
 465    >>> to_tuple(Foo(i=10))
 466    (10, 'foo', 100.0, True)
 467
 468    You can pass any type supported by pyserde. For example,
 469
 470    >>> lst = [Foo(i=10), Foo(i=20)]
 471    >>> to_tuple(lst)
 472    [(10, 'foo', 100.0, True), (20, 'foo', 100.0, True)]
 473    """
 474    return to_obj(  # type: ignore
 475        o,
 476        named=False,
 477        c=c,
 478        reuse_instances=reuse_instances,
 479        convert_sets=convert_sets,
 480        skip_none=skip_none,
 481    )
 482
 483
 484def asdict(v: Any) -> dict[Any, Any]:
 485    """
 486    Serialize object into dictionary.
 487    """
 488    return to_dict(v, reuse_instances=False, convert_sets=False)
 489
 490
 491def to_dict(
 492    o: Any,
 493    c: type[Any] | None = None,
 494    reuse_instances: bool | None = None,
 495    convert_sets: bool | None = None,
 496    skip_none: bool = False,
 497) -> dict[Any, Any]:
 498    """
 499    Serialize object into python dictionary. This function ensures that the dataclass's fields are
 500    accurately represented as key-value pairs in the resulting dictionary.
 501
 502    * `o`: Any pyserde object that you want to convert to `dict`
 503    * `c`: Optional class argument
 504    * `reuse_instances`: pyserde will pass instances (e.g. Path, datetime) directly to serializer
 505    instead of converting them to serializable representation e.g. string. This behaviour allows
 506    to delegate serializtation to underlying data format packages e.g. `pyyaml` and potentially
 507    improve performance.
 508    * `convert_sets`: This option controls how sets are handled during serialization and
 509    deserialization. When `convert_sets` is set to True, pyserde will convert sets to lists during
 510    serialization and back to sets during deserialization. This is useful for data formats that
 511    do not natively support sets.
 512    * `skip_none`: When set to True, any field in the class with a None value is excluded from the
 513    serialized output. Defaults to False.
 514
 515    >>> from serde import serde
 516    >>> @serde
 517    ... class Foo:
 518    ...     i: int
 519    ...     s: str = 'foo'
 520    ...     f: float = 100.0
 521    ...     b: bool = True
 522    >>>
 523    >>> to_dict(Foo(i=10))
 524    {'i': 10, 's': 'foo', 'f': 100.0, 'b': True}
 525
 526    You can serialize not only pyserde objects but also objects of any supported types. For example,
 527    the following example serializes list of pyserde objects into dict.
 528
 529    >>> lst = [Foo(i=10), Foo(i=20)]
 530    >>> to_dict(lst)
 531    [{'i': 10, 's': 'foo', 'f': 100.0, 'b': True}, {'i': 20, 's': 'foo', 'f': 100.0, 'b': True}]
 532    """
 533    return to_obj(  # type: ignore
 534        o,
 535        named=True,
 536        c=c,
 537        reuse_instances=reuse_instances,
 538        convert_sets=convert_sets,
 539        skip_none=skip_none,
 540    )
 541
 542
 543@dataclass
 544class SeField(Field[T]):
 545    """
 546    Field class for serialization.
 547    """
 548
 549    @property
 550    def varname(self) -> str:
 551        """
 552        Get variable name in the generated code e.g. obj.a.b
 553        """
 554        var = getattr(self.parent, "varname", None) if self.parent else None
 555        if var:
 556            return f"{var}.{self.name}"
 557        else:
 558            if self.name is None:
 559                raise SerdeError("Field name is None.")
 560            return self.name
 561
 562    def __getitem__(self, n: int) -> SeField[Any]:
 563        typ = type_args(self.type)[n]
 564        opts: dict[str, Any] = {
 565            "kw_only": self.kw_only,
 566            "case": self.case,
 567            "alias": self.alias,
 568            "rename": self.rename,
 569            "skip": self.skip,
 570            "skip_if": self.skip_if,
 571            "skip_if_false": self.skip_if_false,
 572            "skip_if_default": self.skip_if_default,
 573            "serializer": self.serializer,
 574            "deserializer": self.deserializer,
 575            "flatten": self.flatten,
 576        }
 577        return SeField(typ, name=None, **opts)
 578
 579
 580def sefields(cls: type[Any], serialize_class_var: bool = False) -> Iterator[SeField[Any]]:
 581    """
 582    Iterate fields for serialization.
 583    """
 584    for f in fields(SeField, cls, serialize_class_var=serialize_class_var):
 585        f.parent = SeField(None, "obj")  # type: ignore
 586        yield f
 587
 588
 589jinja2_env = jinja2.Environment(
 590    loader=jinja2.DictLoader(
 591        {
 592            "dict": """
 593def {{func}}(obj, reuse_instances = None, convert_sets = None, skip_none = False):
 594  if reuse_instances is None:
 595    reuse_instances = {{serde_scope.reuse_instances_default}}
 596  if convert_sets is None:
 597    convert_sets = {{serde_scope.convert_sets_default}}
 598  if not is_dataclass(obj):
 599    return copy.deepcopy(obj)
 600
 601  res = {}
 602  {% for f in fields -%}
 603  {% if not f.skip -%}
 604  subres = {{rvalue(f)}}
 605  {% if lvalue(f) == '__FLATTEN_DICT__' -%}
 606  # Merge flattened dict into result (declared fields take precedence)
 607  if subres:
 608    for __k, __v in subres.items():
 609      if __k not in res:
 610        res[__k] = __v
 611  {% elif f.skip_if -%}
 612  if not {{f.skip_if.name}}(subres):
 613    {{lvalue(f)}} = subres
 614  {% else -%}
 615  if skip_none:
 616    if subres is not None:
 617      {{lvalue(f)}} = subres
 618  else:
 619    {{lvalue(f)}} = subres
 620  {% endif -%}
 621  {% endif %}
 622
 623  {% endfor -%}
 624  return res
 625""",
 626            "transparent_dict": """
 627def {{func}}(obj, reuse_instances = None, convert_sets = None, skip_none = False):
 628  if reuse_instances is None:
 629    reuse_instances = {{serde_scope.reuse_instances_default}}
 630  if convert_sets is None:
 631    convert_sets = {{serde_scope.convert_sets_default}}
 632  if not is_dataclass(obj):
 633    return copy.deepcopy(obj)
 634
 635  res = {{rvalue(field)}}
 636  if skip_none and res is None:
 637    return None
 638  return res
 639""",
 640            "iter": """
 641def {{func}}(obj, reuse_instances=None, convert_sets=None, skip_none=False):
 642  if reuse_instances is None:
 643    reuse_instances = {{serde_scope.reuse_instances_default}}
 644  if convert_sets is None:
 645    convert_sets = {{serde_scope.convert_sets_default}}
 646  if not is_dataclass(obj):
 647    return copy.deepcopy(obj)
 648
 649  return (
 650  {% for f in fields -%}
 651  {% if not f.skip|default(False) %}
 652  {{rvalue(f)}},
 653  {% endif -%}
 654  {% endfor -%}
 655  )
 656""",
 657            "transparent_iter": """
 658def {{func}}(obj, reuse_instances=None, convert_sets=None, skip_none=False):
 659  if reuse_instances is None:
 660    reuse_instances = {{serde_scope.reuse_instances_default}}
 661  if convert_sets is None:
 662    convert_sets = {{serde_scope.convert_sets_default}}
 663  if not is_dataclass(obj):
 664    return copy.deepcopy(obj)
 665
 666  res = {{rvalue(field)}}
 667  if skip_none and res is None:
 668    return None
 669  return res
 670""",
 671            "union": """
 672def {{func}}(obj, reuse_instances, convert_sets, skip_none=False):
 673  union_args = serde_scope.union_se_args['{{func}}']
 674
 675  {% for t in union_args %}
 676  if is_instance(obj, union_args[{{loop.index0}}]):
 677    {% if tagging.is_external() and is_taggable(t) %}
 678    return {"{{typename(t)}}": {{rvalue(arg(t))}}}
 679
 680    {% elif tagging.is_internal() and is_taggable(t) %}
 681    res = {{rvalue(arg(t))}}
 682    res["{{tagging.tag}}"] = "{{typename(t)}}"
 683    return res
 684
 685    {% elif tagging.is_adjacent() and is_taggable(t) %}
 686    res = {"{{tagging.content}}": {{rvalue(arg(t))}}}
 687    res["{{tagging.tag}}"] = "{{typename(t)}}"
 688    return res
 689
 690    {% else %}
 691    return {{rvalue(arg(t))}}
 692    {% endif %}
 693  {% endfor %}
 694  raise SerdeError("Can not serialize " + \
 695                   repr(obj) + \
 696                   " of type " + \
 697                   typename(type(obj)) + \
 698                   " for {{union_name}}")
 699""",
 700        }
 701    )
 702)
 703
 704
 705def render_to_tuple(
 706    cls: type[Any],
 707    legacy_class_serializer: SerializeFunc | None = None,
 708    type_check: TypeCheck = strict,
 709    serialize_class_var: bool = False,
 710    class_serializer: ClassSerializer | None = None,
 711) -> str:
 712    renderer = Renderer(
 713        TO_ITER,
 714        legacy_class_serializer,
 715        suppress_coerce=(not type_check.is_coerce()),
 716        serialize_class_var=serialize_class_var,
 717        class_serializer=class_serializer,
 718        class_name=typename(cls),
 719    )
 720    serde_scope = getattr(cls, SERDE_SCOPE)
 721    if serde_scope.transparent:
 722        transparent = [f for f in sefields(cls, serialize_class_var) if not f.skip]
 723        return jinja2_env.get_template("transparent_iter").render(
 724            func=TO_ITER,
 725            serde_scope=serde_scope,
 726            field=transparent[0],
 727            type_check=type_check,
 728            rvalue=renderer.render,
 729        )
 730    else:
 731        return jinja2_env.get_template("iter").render(
 732            func=TO_ITER,
 733            serde_scope=serde_scope,
 734            fields=sefields(cls, serialize_class_var),
 735            type_check=type_check,
 736            rvalue=renderer.render,
 737        )
 738
 739
 740def render_to_dict(
 741    cls: type[Any],
 742    case: str | None = None,
 743    legacy_class_serializer: SerializeFunc | None = None,
 744    type_check: TypeCheck = strict,
 745    serialize_class_var: bool = False,
 746    class_serializer: ClassSerializer | None = None,
 747) -> str:
 748    renderer = Renderer(
 749        TO_DICT,
 750        legacy_class_serializer,
 751        suppress_coerce=(not type_check.is_coerce()),
 752        class_serializer=class_serializer,
 753        class_name=typename(cls),
 754    )
 755    lrenderer = LRenderer(case, serialize_class_var)
 756    serde_scope = getattr(cls, SERDE_SCOPE)
 757    if serde_scope.transparent:
 758        transparent = [f for f in sefields(cls, serialize_class_var) if not f.skip]
 759        return jinja2_env.get_template("transparent_dict").render(
 760            func=TO_DICT,
 761            serde_scope=serde_scope,
 762            field=transparent[0],
 763            type_check=type_check,
 764            lvalue=lrenderer.render,
 765            rvalue=renderer.render,
 766        )
 767    else:
 768        return jinja2_env.get_template("dict").render(
 769            func=TO_DICT,
 770            serde_scope=serde_scope,
 771            fields=sefields(cls, serialize_class_var),
 772            type_check=type_check,
 773            lvalue=lrenderer.render,
 774            rvalue=renderer.render,
 775        )
 776
 777
 778def render_union_func(
 779    cls: type[Any], union_args: list[type[Any]], tagging: Tagging = DefaultTagging
 780) -> str:
 781    """
 782    Render function that serializes a field with union type.
 783    """
 784    union_name = f"Union[{', '.join([typename(a) for a in union_args])}]"
 785    renderer = Renderer(TO_DICT, suppress_coerce=True, class_name=typename(cls))
 786    return jinja2_env.get_template("union").render(
 787        func=union_func_name(UNION_SE_PREFIX, union_args),
 788        serde_scope=getattr(cls, SERDE_SCOPE),
 789        union_args=union_args,
 790        union_name=union_name,
 791        tagging=tagging,
 792        is_taggable=Tagging.is_taggable,
 793        arg=lambda x: SeField(x, "obj"),
 794        rvalue=renderer.render,
 795        typename=typename,
 796    )
 797
 798
 799@dataclass
 800class LRenderer:
 801    """
 802    Render lvalue for various types.
 803    """
 804
 805    case: str | None
 806    serialize_class_var: bool = False
 807
 808    def render(self, arg: SeField[Any]) -> str:
 809        """
 810        Render lvalue
 811        """
 812        if is_dataclass(arg.type) and arg.flatten:
 813            return self.flatten(arg)
 814        elif is_opt(arg.type) and arg.flatten:
 815            inner = arg[0]
 816            if is_dataclass(inner.type):
 817                return self.flatten(inner)
 818        elif is_flatten_dict(arg.type) and arg.flatten:
 819            # Special marker for flatten dict - handled by template
 820            return "__FLATTEN_DICT__"
 821
 822        return f'res["{arg.conv_name(self.case)}"]'
 823
 824    def flatten(self, arg: SeField[Any]) -> str:
 825        """
 826        Render field with flatten attribute.
 827        """
 828        flattened = []
 829        for f in sefields(arg.type, self.serialize_class_var):
 830            flattened.append(self.render(f))
 831        return ", ".join(flattened)
 832
 833
 834@dataclass
 835class Renderer:
 836    """
 837    Render rvalue for code generation.
 838    """
 839
 840    func: str
 841    legacy_class_serializer: SerializeFunc | None = None
 842    suppress_coerce: bool = False
 843    """ Suppress type coercing because generated union serializer has its own type checking """
 844    serialize_class_var: bool = False
 845    class_serializer: ClassSerializer | None = None
 846    class_name: str | None = None
 847
 848    def render(self, arg: SeField[Any]) -> str:
 849        """
 850        Render rvalue
 851        """
 852        implemented_methods: dict[type[Any], int] = {}
 853        class_serializers: Iterable[ClassSerializer] = itertools.chain(
 854            GLOBAL_CLASS_SERIALIZER, [self.class_serializer] if self.class_serializer else []
 855        )
 856        for n, class_serializer in enumerate(class_serializers):
 857            for method in class_serializer.__class__.serialize.methods:  # type: ignore
 858                implemented_methods[method.signature.types[1]] = n
 859
 860        custom_serializer_available = arg.type in implemented_methods
 861        if custom_serializer_available and not arg.serializer:
 862            res = f"class_serializers[{implemented_methods[arg.type]}].serialize({arg.varname})"
 863        elif arg.serializer and arg.serializer.inner is not default_serializer:
 864            res = self.custom_field_serializer(arg)
 865        elif is_dataclass(arg.type):
 866            res = self.dataclass(arg)
 867        elif is_opt(arg.type):
 868            res = self.opt(arg)
 869        elif is_list(arg.type):
 870            res = self.list(arg)
 871        elif is_set(arg.type):
 872            res = self.set(arg)
 873        elif is_deque(arg.type):
 874            res = self.deque(arg)
 875        elif is_counter(arg.type):
 876            res = self.counter(arg)
 877        elif is_dict(arg.type):
 878            res = self.dict(arg)
 879        elif is_tuple(arg.type):
 880            res = self.tuple(arg)
 881        elif is_enum(arg.type):
 882            res = self.enum(arg)
 883        elif _is_numpy_datetime(arg.type):
 884            from .numpy import serialize_numpy_datetime
 885
 886            res = serialize_numpy_datetime(arg)
 887        elif _is_numpy_scalar(arg.type):
 888            from .numpy import serialize_numpy_scalar
 889
 890            res = serialize_numpy_scalar(arg)
 891        elif _is_numpy_array(arg.type):
 892            from .numpy import serialize_numpy_array
 893
 894            res = serialize_numpy_array(arg)
 895        elif _is_numpy_jaxtyping(arg.type):
 896            from .numpy import serialize_numpy_array
 897
 898            res = serialize_numpy_array(arg)
 899        elif is_primitive(arg.type):
 900            res = self.primitive(arg)
 901        elif is_union(arg.type):
 902            res = self.union_func(arg)
 903        elif is_str_serializable(arg.type):
 904            res = f"{arg.varname} if reuse_instances else {self.string(arg)}"
 905        elif is_datetime(arg.type):
 906            res = f"{arg.varname} if reuse_instances else {arg.varname}.isoformat()"
 907        elif is_none(arg.type):
 908            res = "None"
 909        elif is_any(arg.type) or is_bearable(arg.type, TypeVar):  # type: ignore[arg-type]  # pyright: ignore
 910            res = f"to_obj({arg.varname}, True, False, False, skip_none, typing.Any)"
 911        elif is_generic(arg.type):
 912            origin = get_origin(arg.type)
 913            assert origin
 914            arg.type = origin
 915            res = self.render(arg)
 916        elif is_literal(arg.type):
 917            res = self.literal(arg)
 918        elif is_class_var(arg.type):
 919            arg.type = type_args(arg.type)[0]
 920            res = self.render(arg)
 921        elif is_pep695_type_alias(arg.type):
 922            res = self.render(dataclasses.replace(arg, type=arg.type.__value__))
 923        else:
 924            raise SerdeError(f"Unsupported type: {typename(arg.type)}")
 925
 926        # Custom field serializer overrides custom class serializer.
 927        if self.legacy_class_serializer and not arg.serializer and not custom_serializer_available:
 928            return (
 929                "serde_legacy_custom_class_serializer("
 930                f"{typename(arg.type)}, "
 931                f"{arg.varname}, "
 932                f"default=lambda: {res})"
 933            )
 934        else:
 935            return res
 936
 937    def custom_field_serializer(self, arg: SeField[Any]) -> str:
 938        """
 939        Render rvalue for the field with custom serializer.
 940        """
 941        assert arg.serializer
 942        return f"{arg.serializer.name}({arg.varname})"
 943
 944    def dataclass(self, arg: SeField[Any]) -> str:
 945        """
 946        Render rvalue for dataclass.
 947        """
 948        if arg.flatten:
 949            flattened = []
 950            for f in sefields(arg.type, self.serialize_class_var):
 951                f.parent = arg
 952                flattened.append(self.render(f))
 953            return ", ".join(flattened)
 954        else:
 955            return (
 956                f"{arg.varname}.{SERDE_SCOPE}.funcs['{self.func}']({arg.varname}, "
 957                "reuse_instances=reuse_instances, convert_sets=convert_sets, skip_none=skip_none)"
 958            )
 959
 960    def opt(self, arg: SeField[Any]) -> str:
 961        """
 962        Render rvalue for optional.
 963        """
 964        if is_bare_opt(arg.type):
 965            return f"{arg.varname} if {arg.varname} is not None else None"
 966
 967        inner = arg[0]
 968        inner.name = arg.varname
 969        if arg.flatten:
 970            return self.render(inner)
 971        else:
 972            return f"({self.render(inner)}) if {arg.varname} is not None else None"
 973
 974    def list(self, arg: SeField[Any]) -> str:
 975        """
 976        Render rvalue for list.
 977        """
 978        if is_bare_list(arg.type):
 979            origin = get_origin(arg.type) or arg.type
 980            return arg.varname if origin is list else f"list({arg.varname})"
 981        else:
 982            earg = arg[0]
 983            earg.name = "v"
 984            return f"[{self.render(earg)} for v in {arg.varname}]"
 985
 986    def set(self, arg: SeField[Any]) -> str:
 987        """
 988        Render rvalue for set.
 989        """
 990        if is_bare_set(arg.type):
 991            return f"list({arg.varname}) if convert_sets else {arg.varname}"
 992        else:
 993            earg = arg[0]
 994            earg.name = "v"
 995            return (
 996                f"[{self.render(earg)} for v in {arg.varname}] "
 997                f"if convert_sets else set({self.render(earg)} for v in {arg.varname})"
 998            )
 999
1000    def deque(self, arg: SeField[Any]) -> str:
1001        """
1002        Render rvalue for deque.
1003        """
1004        if is_bare_deque(arg.type):
1005            return f"list({arg.varname})"
1006        else:
1007            earg = arg[0]
1008            earg.name = "v"
1009            return f"[{self.render(earg)} for v in {arg.varname}]"
1010
1011    def counter(self, arg: SeField[Any]) -> str:
1012        """
1013        Render rvalue for Counter.
1014        """
1015        if is_bare_counter(arg.type):
1016            return f"dict({arg.varname})"
1017        else:
1018            karg = arg[0]
1019            karg.name = "k"
1020            return f"{{{self.render(karg)}: v for k, v in {arg.varname}.items()}}"
1021
1022    def tuple(self, arg: SeField[Any]) -> str:
1023        """
1024        Render rvalue for tuple.
1025        """
1026        if is_bare_tuple(arg.type):
1027            return arg.varname
1028        elif is_variable_tuple(arg.type):
1029            earg = arg[0]
1030            earg.name = "v"
1031            return f"tuple({self.render(earg)} for v in {arg.varname})"
1032        else:
1033            rvalues = []
1034            for i, _ in enumerate(type_args(arg.type)):
1035                r = arg[i]
1036                r.name = f"{arg.varname}[{i}]"
1037                rvalues.append(self.render(r))
1038            return f"({', '.join(rvalues)},)"  # trailing , is required for single element tuples
1039
1040    def dict(self, arg: SeField[Any]) -> str:
1041        """
1042        Render rvalue for dict.
1043        """
1044        if is_bare_dict(arg.type):
1045            return arg.varname
1046        else:
1047            karg = arg[0]
1048            karg.name = "k"
1049            varg = arg[1]
1050            varg.name = "v"
1051            return f"{{{self.render(karg)}: {self.render(varg)} for k, v in {arg.varname}.items()}}"
1052
1053    def enum(self, arg: SeField[Any]) -> str:
1054        return f"enum_value({typename(arg.type)}, {arg.varname})"
1055
1056    def primitive(self, arg: SeField[Any]) -> str:
1057        """
1058        Render rvalue for primitives.
1059        """
1060        typ = typename(arg.type)
1061        var = arg.varname
1062        if self.suppress_coerce:
1063            return var
1064        else:
1065            assert arg.name
1066            escaped_arg_name = arg.name.replace('"', '\\"')
1067            return f'coerce_object("{self.class_name}", "{escaped_arg_name}", {typ}, {var})'
1068
1069    def string(self, arg: SeField[Any]) -> str:
1070        return f"str({arg.varname})"
1071
1072    def union_func(self, arg: SeField[Any]) -> str:
1073        func_name = union_func_name(UNION_SE_PREFIX, list(type_args(arg.type)))
1074        return (
1075            f"serde_scope.funcs['{func_name}']({arg.varname}, "
1076            "reuse_instances, convert_sets, skip_none)"
1077        )
1078
1079    def literal(self, arg: SeField[Any]) -> str:
1080        return f"{arg.varname}"
1081
1082
1083def enum_value(cls: Any, e: Any) -> Any:
1084    """
1085    Helper function to get value from enum or enum compatible value.
1086    """
1087    if is_enum(e):
1088        v = e.value
1089        # Recursively get value of Nested enum.
1090        if is_enum(v):
1091            return enum_value(v.__class__, v)
1092        else:
1093            return v
1094    else:
1095        return cls(e).value
@dataclass_transform()
def serialize( _cls: type[~T] | None = None, rename_all: str | None = None, reuse_instances_default: bool = False, convert_sets_default: bool = False, serializer: Callable[[type[typing.Any], typing.Any], typing.Any] | None = None, tagging: serde.core.Tagging = Tagging(tag=None, content=None, kind=<Kind.External: 1>), type_check: serde.core.TypeCheck = TypeCheck(kind=<Kind.Strict: 3>), serialize_class_var: bool = False, transparent: bool = False, class_serializer: serde.ClassSerializer | None = None, **kwargs: Any) -> type[~T]:
196@dataclass_transform()
197def serialize(
198    _cls: type[T] | None = None,
199    rename_all: str | None = None,
200    reuse_instances_default: bool = False,
201    convert_sets_default: bool = False,
202    serializer: SerializeFunc | None = None,
203    tagging: Tagging = DefaultTagging,
204    type_check: TypeCheck = strict,
205    serialize_class_var: bool = False,
206    transparent: bool = False,
207    class_serializer: ClassSerializer | None = None,
208    **kwargs: Any,
209) -> type[T]:
210    """
211    A dataclass with this decorator is serializable into any of the data formats
212    supported by pyserde.
213
214    >>> from datetime import datetime
215    >>> from serde import serialize
216    >>> from serde.json import to_json
217    >>>
218    >>> @serialize
219    ... class Foo:
220    ...     i: int
221    ...     s: str
222    ...     f: float
223    ...     b: bool
224    >>>
225    >>> to_json(Foo(i=10, s='foo', f=100.0, b=True))
226    '{"i":10,"s":"foo","f":100.0,"b":true}'
227    """
228
229    def wrap(cls: type[T]) -> type[T]:
230        tagging.check()
231
232        # If no `dataclass` found in the class, dataclassify it automatically.
233        if not is_dataclass(cls):
234            dataclass(cls)
235
236        if transparent:
237            get_transparent_field(cls)
238
239        if type_check.is_strict():
240            serde_beartype = beartype(conf=BeartypeConf(violation_type=SerdeError))
241            serde_beartype(cls)
242
243        g: dict[str, Any] = {}
244
245        # Create a scope storage used by serde.
246        # Each class should get own scope. Child classes can not share scope with parent class.
247        # That's why we need the "scope.cls is not cls" check.
248        scope: Scope | None = getattr(cls, SERDE_SCOPE, None)
249        if scope is None or scope.cls is not cls:
250            scope = Scope(
251                cls,
252                reuse_instances_default=reuse_instances_default,
253                convert_sets_default=convert_sets_default,
254            )
255            setattr(cls, SERDE_SCOPE, scope)
256        scope.transparent = transparent
257
258        class_serializers: list[ClassSerializer] = list(
259            itertools.chain(GLOBAL_CLASS_SERIALIZER, [class_serializer] if class_serializer else [])
260        )
261
262        # Set some globals for all generated functions
263        g["cls"] = cls
264        g["copy"] = copy
265        g["serde_scope"] = scope
266        g["SerdeError"] = SerdeError
267        g["enum_value"] = enum_value
268        g["is_dataclass"] = is_dataclass
269        g["typename"] = typename  # used in union functions
270        g["is_instance"] = is_instance  # used in union functions
271        g["to_obj"] = to_obj
272        g["typing"] = typing
273        g["Literal"] = Literal
274        g["TypeCheck"] = TypeCheck
275        g["disabled"] = disabled
276        g["coerce_object"] = coerce_object
277        g["class_serializers"] = class_serializers
278        if serializer:
279            g["serde_legacy_custom_class_serializer"] = functools.partial(
280                serde_legacy_custom_class_serializer, custom=serializer
281            )
282
283        # Collect types used in the generated code.
284        for typ in iter_types(cls):
285            # When we encounter a dataclass not marked with serialize, then also generate serialize
286            # functions for it.
287            if is_dataclass_without_se(typ) and typ is not cls:
288                # We call serialize and not wrap to make sure that we will use the default serde
289                # configuration for generating the serialization function.
290                serialize(typ)
291
292            if is_primitive(typ) and not is_enum(typ):
293                continue
294            g[typename(typ)] = typ
295
296        # render all union functions
297        for union in iter_unions(cls):
298            union_args = list(type_args(union))
299            union_key = union_func_name(UNION_SE_PREFIX, union_args)
300            add_func(scope, union_key, render_union_func(cls, union_args, tagging), g)
301            scope.union_se_args[union_key] = union_args
302
303        for f in sefields(cls, serialize_class_var):
304            if f.skip_if:
305                g[f.skip_if.name] = f.skip_if
306            if f.serializer:
307                g[f.serializer.name] = f.serializer
308
309        add_func(
310            scope,
311            TO_ITER,
312            render_to_tuple(cls, serializer, type_check, serialize_class_var, class_serializer),
313            g,
314        )
315        add_func(
316            scope,
317            TO_DICT,
318            render_to_dict(
319                cls, rename_all, serializer, type_check, serialize_class_var, class_serializer
320            ),
321            g,
322        )
323
324        logger.debug(f"{typename(cls)}: {SERDE_SCOPE} {scope}")
325
326        return cls
327
328    if _cls is None:
329        return wrap  # type: ignore
330
331    if _cls in GENERATION_STACK:
332        return _cls
333
334    GENERATION_STACK.append(_cls)
335    try:
336        return wrap(_cls)
337    finally:
338        GENERATION_STACK.pop()

A dataclass with this decorator is serializable into any of the data formats supported by pyserde.

>>> from datetime import datetime
>>> from serde import serialize
>>> from serde.json import to_json
>>>
>>> @serialize
... class Foo:
...     i: int
...     s: str
...     f: float
...     b: bool
>>>
>>> to_json(Foo(i=10, s='foo', f=100.0, b=True))
'{"i":10,"s":"foo","f":100.0,"b":true}'
def is_serializable(instance_or_class: Any) -> bool:
341def is_serializable(instance_or_class: Any) -> bool:
342    """
343    Test if an instance or class is serializable.
344
345    >>> @serialize
346    ... class Foo:
347    ...     pass
348
349    Testing `Foo` class object returns `True`.
350    >>> is_serializable(Foo)
351    True
352
353    Testing `Foo` object also returns `True`.
354    >>> is_serializable(Foo())
355    True
356    """
357    return hasattr(instance_or_class, SERDE_SCOPE)

Test if an instance or class is serializable.

>>> @serialize
... class Foo:
...     pass

Testing Foo class object returns True.

>>> is_serializable(Foo)
True

Testing Foo object also returns True.

>>> is_serializable(Foo())
True
def to_dict( o: Any, c: type[typing.Any] | None = None, reuse_instances: bool | None = None, convert_sets: bool | None = None, skip_none: bool = False) -> dict[typing.Any, typing.Any]:
492def to_dict(
493    o: Any,
494    c: type[Any] | None = None,
495    reuse_instances: bool | None = None,
496    convert_sets: bool | None = None,
497    skip_none: bool = False,
498) -> dict[Any, Any]:
499    """
500    Serialize object into python dictionary. This function ensures that the dataclass's fields are
501    accurately represented as key-value pairs in the resulting dictionary.
502
503    * `o`: Any pyserde object that you want to convert to `dict`
504    * `c`: Optional class argument
505    * `reuse_instances`: pyserde will pass instances (e.g. Path, datetime) directly to serializer
506    instead of converting them to serializable representation e.g. string. This behaviour allows
507    to delegate serializtation to underlying data format packages e.g. `pyyaml` and potentially
508    improve performance.
509    * `convert_sets`: This option controls how sets are handled during serialization and
510    deserialization. When `convert_sets` is set to True, pyserde will convert sets to lists during
511    serialization and back to sets during deserialization. This is useful for data formats that
512    do not natively support sets.
513    * `skip_none`: When set to True, any field in the class with a None value is excluded from the
514    serialized output. Defaults to False.
515
516    >>> from serde import serde
517    >>> @serde
518    ... class Foo:
519    ...     i: int
520    ...     s: str = 'foo'
521    ...     f: float = 100.0
522    ...     b: bool = True
523    >>>
524    >>> to_dict(Foo(i=10))
525    {'i': 10, 's': 'foo', 'f': 100.0, 'b': True}
526
527    You can serialize not only pyserde objects but also objects of any supported types. For example,
528    the following example serializes list of pyserde objects into dict.
529
530    >>> lst = [Foo(i=10), Foo(i=20)]
531    >>> to_dict(lst)
532    [{'i': 10, 's': 'foo', 'f': 100.0, 'b': True}, {'i': 20, 's': 'foo', 'f': 100.0, 'b': True}]
533    """
534    return to_obj(  # type: ignore
535        o,
536        named=True,
537        c=c,
538        reuse_instances=reuse_instances,
539        convert_sets=convert_sets,
540        skip_none=skip_none,
541    )

Serialize object into python dictionary. This function ensures that the dataclass's fields are accurately represented as key-value pairs in the resulting dictionary.

  • o: Any pyserde object that you want to convert to dict
  • c: Optional class argument
  • reuse_instances: pyserde will pass instances (e.g. Path, datetime) directly to serializer instead of converting them to serializable representation e.g. string. This behaviour allows to delegate serializtation to underlying data format packages e.g. pyyaml and potentially improve performance.
  • convert_sets: This option controls how sets are handled during serialization and deserialization. When convert_sets is set to True, pyserde will convert sets to lists during serialization and back to sets during deserialization. This is useful for data formats that do not natively support sets.
  • skip_none: When set to True, any field in the class with a None value is excluded from the serialized output. Defaults to False.
>>> from serde import serde
>>> @serde
... class Foo:
...     i: int
...     s: str = 'foo'
...     f: float = 100.0
...     b: bool = True
>>>
>>> to_dict(Foo(i=10))
{'i': 10, 's': 'foo', 'f': 100.0, 'b': True}

You can serialize not only pyserde objects but also objects of any supported types. For example, the following example serializes list of pyserde objects into dict.

>>> lst = [Foo(i=10), Foo(i=20)]
>>> to_dict(lst)
[{'i': 10, 's': 'foo', 'f': 100.0, 'b': True}, {'i': 20, 's': 'foo', 'f': 100.0, 'b': True}]
def to_tuple( o: Any, c: type[typing.Any] | None = None, reuse_instances: bool | None = None, convert_sets: bool | None = None, skip_none: bool = False) -> tuple[typing.Any, ...]:
449def to_tuple(
450    o: Any,
451    c: type[Any] | None = None,
452    reuse_instances: bool | None = None,
453    convert_sets: bool | None = None,
454    skip_none: bool = False,
455) -> tuple[Any, ...]:
456    """
457    Serialize object into tuple.
458
459    >>> @serialize
460    ... class Foo:
461    ...     i: int
462    ...     s: str = 'foo'
463    ...     f: float = 100.0
464    ...     b: bool = True
465    >>>
466    >>> to_tuple(Foo(i=10))
467    (10, 'foo', 100.0, True)
468
469    You can pass any type supported by pyserde. For example,
470
471    >>> lst = [Foo(i=10), Foo(i=20)]
472    >>> to_tuple(lst)
473    [(10, 'foo', 100.0, True), (20, 'foo', 100.0, True)]
474    """
475    return to_obj(  # type: ignore
476        o,
477        named=False,
478        c=c,
479        reuse_instances=reuse_instances,
480        convert_sets=convert_sets,
481        skip_none=skip_none,
482    )

Serialize object into tuple.

>>> @serialize
... class Foo:
...     i: int
...     s: str = 'foo'
...     f: float = 100.0
...     b: bool = True
>>>
>>> to_tuple(Foo(i=10))
(10, 'foo', 100.0, True)

You can pass any type supported by pyserde. For example,

>>> lst = [Foo(i=10), Foo(i=20)]
>>> to_tuple(lst)
[(10, 'foo', 100.0, True), (20, 'foo', 100.0, True)]