Package: mashumaro
Fast and well tested serialization libraryIn Python, you often need to dump and load objects based on the schema you
have. It can be a dataclass model, a list of third-party generic classes or
whatever. Mashumaro not only lets you save and load things in different ways,
but it also does itsuper quick.Key features🚀 One of the fastest libraries☝️ Mature and time-tested👶 Easy to use out of the box⚙️ Highly customizable🎉 Built-in support for JSON, YAML, TOML, MessagePack📦 Built-in support for almost all Python types including typing-extensions📝 JSON Schema generationTable of contentsIntroductionInstallationChangelogSupported data typesUsage exampleHow does it work?BenchmarkSupported serialization formatsBasic formJSONYAMLTOMLMessagePackCustomizationSerializableTypeinterfaceUser-defined typesUser-defined generic typesSerializationStrategyThird-party typesThird-party generic typesField optionsserializeoptiondeserializeoptionserialization_strategyoptionaliasoptionConfig optionsdebugconfig optioncode_generation_optionsconfig optionserialization_strategyconfig optionaliasesconfig optionserialize_by_aliasconfig optionallow_deserialization_not_by_aliasconfig optionomit_noneconfig optionomit_defaultconfig optionnamedtuple_as_dictconfig optionallow_postponed_evaluationconfig optiondialectconfig optionorjson_optionsconfig optiondiscriminatorconfig optionlazy_compilationconfig optionsort_keysconfig optionPassing field values as isExtending existing typesDialectsserialization_strategydialect optionserialize_by_aliasdialect optionomit_nonedialect optionomit_defaultdialect optionnamed_tuple_as_dictdialect optionno_copy_collectionsdialect optionChanging the default dialectDiscriminatorSubclasses distinguishable by a fieldSubclasses without a common fieldClass level discriminatorWorking with union of classesUsing a custom variant tagger functionCode generation optionsAddomit_nonekeyword argumentAddby_aliaskeyword argumentAdddialectkeyword argumentAddcontextkeyword argumentGeneric dataclassesGeneric dataclass inheritanceGeneric dataclass in a field typeGenericSerializableTypeinterfaceSerialization hooksBefore deserializationAfter deserializationBefore serializationAfter serializationJSON SchemaBuilding JSON SchemaJSON Schema constraintsExtending JSON SchemaJSON Schema and custom serialization methodsIntroductionThis library provides two fundamentally different approaches to converting
your data to and from various formats. Each of them is useful in different
situations:CodecsMixinsCodecs are represented by a set of decoder / encoder classes and
decode / encode functions for each supported format. You can use them
to convert data of any python built-in and third-party type to JSON, YAML,
TOML, MessagePack or a basic form accepted by other serialization formats.
For example, you can convert a list of datetime objects to JSON array
containing string-represented datetimes and vice versa.Mixins are primarily for dataclass models. They are represented by mixin
classes that add methods for converting to and from JSON, YAML, TOML,
MessagePack or a basic form accepted by other serialization formats.
If you have a root dataclass model, then it will be the easiest way to make it
serializable. All you have to do is inherit a particular mixin class.In addition to serialization functionality, this library also provides JSON
Schema builder that can be used in places where interoperability matters.InstallationUse pip to install:$pipinstallmashumaroThe current version ofmashumarosupports Python versions 3.8 — 3.12.It's not recommended to use any version of Python that has reached itsend of lifeand is no longer receiving
security updates or bug fixes from the Python development team.
For convenience, there is a table below that outlines the
last version ofmashumarothat can be installed on unmaintained versions
of Python.Python VersionLast Version of mashumaroPython EOL3.73.9.12023-06-273.63.1.12021-12-23ChangelogThis project follows the principles ofSemantic Versioning.
Changelog is available onGitHub Releases page.Supported data typesThere is support for generic types from the standardtypingmodule:ListTupleNamedTupleSetFrozenSetDequeDictOrderedDictDefaultDictTypedDictMappingMutableMappingCounterChainMapSequencefor standard generic types onPEP 585compatible Python (3.9+):listtuplenamedtuplesetfrozensetcollections.abc.Setcollections.abc.MutableSetcollections.dequedictcollections.OrderedDictcollections.defaultdictcollections.abc.Mappingcollections.abc.MutableMappingcollections.Countercollections.ChainMapcollections.abc.Sequencecollections.abc.MutableSequencefor special primitives from thetypingmodule:AnyOptionalUnionTypeVarTypeVarTupleNewTypeAnnotatedLiteralLiteralStringFinalSelfUnpackfor standard interpreter types fromtypesmodule:NoneTypeUnionTypefor enumerations based on classes from the standardenummodule:EnumIntEnumStrEnumFlagIntFlagfor common built-in types:intfloatboolstrbytesbytearrayfor built-in datetime oriented types (seemoredetails):datetimedatetimetimedeltatimezoneZoneInfofor pathlike types:PurePathPathPurePosixPathPosixPathPureWindowsPathWindowsPathos.PathLikefor other less popular built-in types:uuid.UUIDdecimal.Decimalfractions.Fractionipaddress.IPv4Addressipaddress.IPv6Addressipaddress.IPv4Networkipaddress.IPv6Networkipaddress.IPv4Interfaceipaddress.IPv6Interfacefor backported types fromtyping-extensions:OrderedDictTypedDictAnnotatedLiteralLiteralStringSelfTypeVarTupleUnpackfor arbitrary types:user-defined typesthird-party typesuser-defined generic typesthird-party generic typesUsage exampleSuppose we're developing a financial application and we operate with currencies
and stocks:fromdataclassesimportdataclassfromenumimportEnumclassCurrency(Enum):USD="USD"EUR="EUR"@dataclassclassCurrencyPosition:currency:Currencybalance:float@dataclassclassStockPosition:ticker:strname:strbalance:intNow we want a dataclass for portfolio that will be serialized to and from JSON.
We inheritDataClassJSONMixinthat adds this functionality:frommashumaro.mixins.jsonimportDataClassJSONMixin...@dataclassclassPortfolio(DataClassJSONMixin):currencies:list[CurrencyPosition]stocks:list[StockPosition]Let's create a portfolio instance and check methodsfrom_jsonandto_json:portfolio=Portfolio(currencies=[CurrencyPosition(Currency.USD,238.67),CurrencyPosition(Currency.EUR,361.84),],stocks=[StockPosition("AAPL","Apple",10),StockPosition("AMZN","Amazon",10),])portfolio_json=portfolio.to_json()assertPortfolio.from_json(portfolio_json)==portfolioIf we need to serialize something different from a root dataclass,
we can use codecs. In the following example we create a JSON decoder and encoder
for a list of currencies:frommashumaro.codecs.jsonimportJSONDecoder,JSONEncoder...decoder=JSONDecoder(list[CurrencyPosition])encoder=JSONEncoder(list[CurrencyPosition])currencies=[CurrencyPosition(Currency.USD,238.67),CurrencyPosition(Currency.EUR,361.84),]currencies_json=encoder.encode(currencies)assertdecoder.decode(currencies_json)==currenciesHow does it work?This library works by taking the schema of the data and generating a
specific decoder and encoder for exactly that schema, taking into account the
specifics of serialization format. This is much faster than inspection of
data types on every call of decoding or encoding at runtime.These specific decoders and encoders are generated bycodecs and mixins. When using serialization
mixins, these methods are compiled during import time (or at runtime in some
cases) and are set as attributes to your dataclasses. To minimize the import
time, you can explicitly enablelazy compilation.BenchmarkmacOS 14.0 SonomaApple M116GB RAMPython 3.12.0Benchmark usingpyperfwith GitHub Issue model. Please note that the
following charts use logarithmic scale, as it is convenient for displaying
very large ranges of values.[!NOTE]Benchmark results may vary depending on the specific configuration and
parameters used for serialization and deserialization. However, we have made
an attempt to use the available options that can speed up and smooth out the
differences in how libraries work.To run benchmark in your environment:gitclonegit@github.com:Fatal1ty/mashumaro.gitcdmashumaro
python3-mvenvenv&&sourceenv/bin/activate
pipinstall-e.
pipinstall-rrequirements-dev.txt
./benchmark/run.shSupported serialization formatsThis library has built-in support for multiple popular formats:JSONYAMLTOMLMessagePackThere are preconfigured codecs and mixin classes. However, you're free
to override some settings if necessary.[!IMPORTANT]As for codecs, you are
offered to choose between convenience and efficiency. When you need to decode
or encode structured data more than once, it's highly recommended to create
and reuse a decoder or encoder specifically for that structure. For one-time
use with default settings it may be convenient to use global functions that
create a disposable decoder or encoder under the hood. Be aware not to use
these convenient global functions multiple times for the same structure.Basic formBasic form denotes a python object consisting only of basic data types
supported by most serialization formats. These types are:str,int,float,bool,list,dict.This is also a starting point you can play with for a comprehensive
transformation of your data.Efficient decoder and encoder can be used as follows:frommashumaro.codecsimportBasicDecoder,BasicEncoder# or from mashumaro.codecs.basic import BasicDecoder, BasicEncoderdecoder=BasicDecoder(<shape_type>,...)decoder.decode(...)encoder=BasicEncoder(<shape_type>,...)encoder.encode(...)Convenient functions are recommended to be used as follows:importmashumaro.codecs.basicasbasic_codecbasic_codec.decode(...,<shape_type>)basic_codec.encode(...,<shape_type>)Mixin can be used as follows:frommashumaroimportDataClassDictMixin# or from mashumaro.mixins.dict import DataClassDictMixin@dataclassclassMyModel(DataClassDictMixin):...MyModel.from_dict(...)MyModel(...).to_dict()[!TIP]You don't need to inheritDataClassDictMixinalong with other serialization
mixins because it's a base class for them.JSONJSONis a lightweight data-interchange format. You can
choose between standard libraryjsonfor compatibility and
third-party dependencyorjsonfor better
performance.json libraryEfficient decoder and encoder can be used as follows:frommashumaro.codecs.jsonimportJSONDecoder,JSONEncoderdecoder=JSONDecoder(<shape_type>,...)decoder.decode(...)encoder=JSONEncoder(<shape_type>,...)encoder.encode(...)Convenient functions can be used as follows:frommashumaro.codecs.jsonimportjson_decode,json_encodejson_decode(...,<shape_type>)json_encode(...,<shape_type>)Convenient function aliases are recommended to be used as follows:importmashumaro.codecs.jsonasjson_codecjson_codec.decode(...<shape_type>)json_codec.encode(...,<shape_type>)Mixin can be used as follows:frommashumaro.mixins.jsonimportDataClassJSONMixin@dataclassclassMyModel(DataClassJSONMixin):...MyModel.from_json(...)MyModel(...).to_json()orjson libraryIn order to useorjsonlibrary, it must
be installed manually or using an extra option formashumaro:pipinstallmashumaro[orjson]The following data types will be handled byorjsonlibrary by default:datetimedatetimeuuid.UUIDEfficient decoder and encoder can be used as follows:frommashumaro.codecs.orjsonimportORJSONDecoder,ORJSONEncoderdecoder=ORJSONDecoder(<shape_type>,...)decoder.decode(...)encoder=ORJSONEncoder(<shape_type>,...)encoder.encode(...)Convenient functions can be used as follows:frommashumaro.codecs.orjsonimportjson_decode,json_encodejson_decode(...,<shape_type>)json_encode(...,<shape_type>)Convenient function aliases are recommended to be used as follows:importmashumaro.codecs.orjsonasjson_codecjson_codec.decode(...<shape_type>)json_codec.encode(...,<shape_type>)Mixin can be used as follows:frommashumaro.mixins.orjsonimportDataClassORJSONMixin@dataclassclassMyModel(DataClassORJSONMixin):...MyModel.from_json(...)MyModel(...).to_json()MyModel(...).to_jsonb()YAMLYAMLis a human-friendly data serialization language for
all programming languages. In order to use this format, thepyyamlpackage must be installed.
You can install it manually or using an extra option formashumaro:pipinstallmashumaro[yaml]Efficient decoder and encoder can be used as follows:frommashumaro.codecs.yamlimportYAMLDecoder,YAMLEncoderdecoder=YAMLDecoder(<shape_type>,...)decoder.decode(...)encoder=YAMLEncoder(<shape_type>,...)encoder.encode(...)Convenient functions can be used as follows:frommashumaro.codecs.yamlimportyaml_decode,yaml_encodeyaml_decode(...,<shape_type>)yaml_encode(...,<shape_type>)Convenient function aliases are recommended to be used as follows:importmashumaro.codecs.yamlasyaml_codecyaml_codec.decode(...<shape_type>)yaml_codec.encode(...,<shape_type>)Mixin can be used as follows:frommashumaro.mixins.yamlimportDataClassYAMLMixin@dataclassclassMyModel(DataClassYAMLMixin):...MyModel.from_yaml(...)MyModel(...).to_yaml()TOMLTOMLis config file format for humans.
In order to use this format, thetomliandtomli-wpackages must be installed.
In Python 3.11+,tomliis included astomlibstandard library
module and is used for this format. You can install the missing packages
manually or using an extra option
formashumaro:pipinstallmashumaro[toml]The following data types will be handled bytomli/tomli-wlibrary by default:datetimedatetimeFields with valueNonewill be omitted on serialization because TOML
doesn't support null values.Efficient decoder and encoder can be used as follows:frommashumaro.codecs.tomlimportTOMLDecoder,TOMLEncoderdecoder=TOMLDecoder(<shape_type>,...)decoder.decode(...)encoder=TOMLEncoder(<shape_type>,...)encoder.encode(...)Convenient functions can be used as follows:frommashumaro.codecs.tomlimporttoml_decode,toml_encodetoml_decode(...,<shape_type>)toml_encode(...,<shape_type>)Convenient function aliases are recommended to be used as follows:importmashumaro.codecs.tomlastoml_codectoml_codec.decode(...<shape_type>)toml_codec.encode(...,<shape_type>)Mixin can be used as follows:frommashumaro.mixins.tomlimportDataClassTOMLMixin@dataclassclassMyModel(DataClassTOMLMixin):...MyModel.from_toml(...)MyModel(...).to_toml()MessagePackMessagePackis an efficient binary serialization format.
In order to use this mixin, themsgpackpackage must be installed. You can install it manually or using an extra
option formashumaro:pipinstallmashumaro[msgpack]The following data types will be handled bymsgpacklibrary by default:bytesbytearrayEfficient decoder and encoder can be used as follows:frommashumaro.codecs.msgpackimportMessagePackDecoder,MessagePackEncoderdecoder=MessagePackDecoder(<shape_type>,...)decoder.decode(...)encoder=MessagePackEncoder(<shape_type>,...)encoder.encode(...)Convenient functions can be used as follows:frommashumaro.codecs.msgpackimportmsgpack_decode,msgpack_encodemsgpack_decode(...,<shape_type>)msgpack_encode(...,<shape_type>)Convenient function aliases are recommended to be used as follows:importmashumaro.codecs.msgpackasmsgpack_codecmsgpack_codec.decode(...<shape_type>)msgpack_codec.encode(...,<shape_type>)Mixin can be used as follows:frommashumaro.mixins.msgpackimportDataClassMessagePackMixin@dataclassclassMyModel(DataClassMessagePackMixin):...MyModel.from_msgpack(...)MyModel(...).to_msgpack()CustomizationCustomization options ofmashumaroare extensive and will most likely cover your needs.
When it comes to non-standard data types and non-standard serialization support, you can do the following:Turn an existing regular or generic class into a serializable one
by inheriting theSerializableTypeclassWrite different serialization strategies for an existing regular or generic type that is not under your control
usingSerializationStrategyclassDefine serialization / deserialization methods:for a specific dataclass field by usingfield optionsfor a specific data type used in the dataclass by usingConfigclassAlter input and output data with serialization / deserializationhooksSeparate serialization scheme from a dataclass in a reusable manner usingdialectsChoose from predefined serialization engines for the specific data types, e.g.datetimeandNamedTupleSerializableType interfaceIf you have a custom class or hierarchy of classes whose instances you want
to serialize withmashumaro, the first option is to implementSerializableTypeinterface.User-defined typesLet's look at this not very practicable example:fromdataclassesimportdataclassfrommashumaroimportDataClassDictMixinfrommashumaro.typesimportSerializableTypeclassAirport(SerializableType):def__init__(self,code,city):self.code,self.city=code,citydef_serialize(self):return[self.code,self.city]@classmethoddef_deserialize(cls,value):returncls(*value)def__eq__(self,other):returnself.code,self.city==other.code,other.city@dataclassclassFlight(DataClassDictMixin):origin:Airportdestination:AirportJFK=Airport("JFK","New York City")LAX=Airport("LAX","Los Angeles")input_data={"origin":["JFK","New York City"],"destination":["LAX","Los Angeles"]}my_flight=Flight.from_dict(input_data)assertmy_flight==Flight(JFK,LAX)assertmy_flight.to_dict()==input_dataYou can see howAirportinstances are seamlessly created from lists of two
strings and serialized into them.By default_deserializemethod will get raw input data without any
transformations before. This should be enough in many cases, especially when
you need to perform non-standard transformations yourself, but let's extend
our example:classItinerary(SerializableType):def__init__(self,flights):self.flights=flightsdef_serialize(self):returnself.flights@classmethoddef_deserialize(cls,flights):returncls(flights)@dataclassclassTravelPlan(DataClassDictMixin):budget:floatitinerary:Itineraryinput_data={"budget":10_000,"itinerary":[{"origin":["JFK","New York City"],"destination":["LAX","Los Angeles"]},{"origin":["LAX","Los Angeles"],"destination":["SFO","San Fransisco"]}]}If we pass the flight list as is intoItinerary._deserialize, our itinerary
will have something that we may not expect —list[dict]instead oflist[Flight]. The solution is quite simple. Instead of callingFlight._deserializeyourself, just use annotations:classItinerary(SerializableType,use_annotations=True):def__init__(self,flights):self.flights=flightsdef_serialize(self)->list[Flight]:returnself.flights@classmethoddef_deserialize(cls,flights:list[Flight]):returncls(flights)my_plan=TravelPlan.from_dict(input_data)assertisinstance(my_plan.itinerary.flights[0],Flight)assertisinstance(my_plan.itinerary.flights[1],Flight)assertmy_plan.to_dict()==input_dataHere we add annotations to the only argument of_deserializemethod and
to the return value of_serializemethod as well. The latter is needed for
correct serialization.[!IMPORTANT]The importance of explicit passinguse_annotations=Truewhen defining a
class is that otherwise implicit using annotations might break compatibility
with old code that wasn't aware of this feature. It will be enabled by
default in the future major release.User-defined generic typesThe great thing to note about using annotations inSerializableTypeis that
they work seamlessly withgenericandvariadic generictypes.
Let's see how this can be useful:fromdatetimeimportdatefromtypingimportTypeVarfromdataclassesimportdataclassfrommashumaroimportDataClassDictMixinfrommashumaro.typesimportSerializableTypeKT=TypeVar("KT")VT=TypeVar("VT")classDictWrapper(dict[KT,VT],SerializableType,use_annotations=True):def_serialize(self)->dict[KT,VT]:returndict(self)@classmethoddef_deserialize(cls,value:dict[KT,VT])->'DictWrapper[KT, VT]':returncls(value)@dataclassclassDataClass(DataClassDictMixin):x:DictWrapper[date,str]y:DictWrapper[str,date]input_data={"x":{"2022-12-07":"2022-12-07"},"y":{"2022-12-07":"2022-12-07"}}obj=DataClass.from_dict(input_data)assertobj==DataClass(x=DictWrapper({date(2022,12,7):"2022-12-07"}),y=DictWrapper({"2022-12-07":date(2022,12,7)}))assertobj.to_dict()==input_dataYou can see that formatted date is deserialized todateobject before passing
toDictWrapper._deserializein a key or value according to the generic
parameters.If you have generic dataclass types, you can useSerializableTypefor them as well, but it's not necessary since
they'resupportedout of the box.SerializationStrategyIf you want to add support for a custom third-party type that is not under your control,
you can write serialization and deserialization logic insideSerializationStrategyclass,
which will be reusable and so well suited in case that third-party type is widely used.SerializationStrategyis also good if you want to create strategies that are slightly different from each other,
because you can add the strategy differentiator in the__init__method.Third-party typesTo demonstrate howSerializationStrategyworks let's write a simple strategy for datetime serialization
in different formats. In this example we will use the same strategy class for two dataclass fields,
but a string representing the date and time will be different.fromdataclassesimportdataclass,fieldfromdatetimeimportdatetimefrommashumaroimportDataClassDictMixin,field_optionsfrommashumaro.typesimportSerializationStrategyclassFormattedDateTime(SerializationStrategy):def__init__(self,fmt):self.fmt=fmtdefserialize(self,value:datetime)->str:returnvalue.strftime(self.fmt)defdeserialize(self,value:str)->datetime:returndatetime.strptime(value,self.fmt)@dataclassclassDateTimeFormats(DataClassDictMixin):short:datetime=field(metadata=field_options(serialization_strategy=FormattedDateTime("%d%m%Y%H%M%S")))verbose:datetime=field(metadata=field_options(serialization_strategy=FormattedDateTime("%A %B%d, %Y, %H:%M:%S")))formats=DateTimeFormats(short=datetime(2019,1,1,12),verbose=datetime(2019,1,1,12),)dictionary=formats.to_dict()# {'short': '01012019120000', 'verbose': 'Tuesday January 01, 2019, 12:00:00'}assertDateTimeFormats.from_dict(dictionary)==formatsSimilarly toSerializableType,SerializationStrategycould also take advantage of annotations:fromdataclassesimportdataclassfromdatetimeimportdatetimefrommashumaroimportDataClassDictMixinfrommashumaro.typesimportSerializationStrategyclassTsSerializationStrategy(SerializationStrategy,use_annotations=True):defserialize(self,value:datetime)->float:returnvalue.timestamp()defdeserialize(self,value:float)->datetime:# value will be converted to float before being passed to this methodreturndatetime.fromtimestamp(value)@dataclassclassExample(DataClassDictMixin):dt:datetimeclassConfig:serialization_strategy={datetime:TsSerializationStrategy(),}example=Example.from_dict({"dt":"1672531200"})print(example)# Example(dt=datetime.datetime(2023, 1, 1, 3, 0))print(example.to_dict())# {'dt': 1672531200.0}Here the passed string value"1672531200"will be converted tofloatbefore being passed todeserializemethod
thanks to thefloatannotation.[!IMPORTANT]As well as forSerializableType, the value ofuse_annotatonswill beTrueby default in the future major release.Third-party generic typesTo create a generic version of a serialization strategy you need to follow these steps:inheritGeneric[...]type
with the number of parameters matching the number of parameters
of the target generic typeWrite generic annotations forserializemethod's return type and fordeserializemethod's argument typeUse the origin type of the target generic type in theserialization_strategyconfig section
(typing.get_originmight be helpful)There is no need to adduse_annotations=Truehere because it's enabled implicitly
for generic serialization strategies.For example, there is a third-partymultidictpackage that has a genericMultiDicttype.
A generic serialization strategy for it might look like this:fromdataclassesimportdataclassfromdatetimeimportdatefrompprintimportpprintfromtypingimportGeneric,List,Tuple,TypeVarfrommashumaroimportDataClassDictMixinfrommashumaro.typesimportSerializationStrategyfrommultidictimportMultiDictT=TypeVar("T")classMultiDictSerializationStrategy(SerializationStrategy,Generic[T]):defserialize(self,value:MultiDict[T])->List[Tuple[str,T]]:return[(k,v)fork,vinvalue.items()]defdeserialize(self,value:List[Tuple[str,T]])->MultiDict[T]:returnMultiDict(value)@dataclassclassExample(DataClassDictMixin):floats:MultiDict[float]date_lists:MultiDict[List[date]]classConfig:serialization_strategy={MultiDict:MultiDictSerializationStrategy()}example=Example(floats=MultiDict([("x",1.1),("x",2.2)]),date_lists=MultiDict([("x",[date(2023,1,1),date(2023,1,2)]),("x",[date(2023,2,1),date(2023,2,2)])]),)pprint(example.to_dict())# {'date_lists': [['x', ['2023-01-01', '2023-01-02']],#                 ['x', ['2023-02-01', '2023-02-02']]],#  'floats': [['x', 1.1], ['x', 2.2]]}assertExample.from_dict(example.to_dict())==exampleField optionsIn some cases creating a new class just for one little thing could be
excessive. Moreover, you may need to deal with third party classes that you are
not allowed to change. You can usedataclasses.fieldfunction to
configure some serialization aspects through itsmetadataparameter. Next
section describes all supported options to use inmetadatamapping.If you don't want to remember the names of the options you can usefield_optionshelper function:fromdataclassesimportdataclass,fieldfrommashumaroimportfield_options@dataclassclassA:x:int=field(metadata=field_options(...))serializeoptionThis option allows you to change the serialization method. When using
this option, the serialization behaviour depends on what type of value the
option has. It could be eitherCallable[[Any], Any]orstr.A value of typeCallable[[Any], Any]is a generic way to specify any callable
object like a function, a class method, a class instance method, an instance
of a callable class or even a lambda function to be called for serialization.A value of typestrsets a specific engine for serialization. Keep in mind
that all possible engines depend on the data type that this option is used
with. At this moment there are next serialization engines to choose from:Applicable data typesSupported enginesDescriptionNamedTuple,namedtupleas_list,as_dictHow to pack named tuples. By defaultas_listengine is used that means your named tuple class instance will be packed into a list of its values. You can pack it into a dictionary usingas_dictengine.AnyomitSkip the field during serialization[!TIP]You can pass a field value as is without changes on serialization usingpass_through.Example:fromdatetimeimportdatetimefromdataclassesimportdataclass,fieldfromtypingimportNamedTuplefrommashumaroimportDataClassDictMixinclassMyNamedTuple(NamedTuple):x:inty:float@dataclassclassA(DataClassDictMixin):dt:datetime=field(metadata={"serialize":lambdav:v.strftime('%Y-%m-%d%H:%M:%S')})t:MyNamedTuple=field(metadata={"serialize":"as_dict"})deserializeoptionThis option allows you to change the deserialization method. When using
this option, the deserialization behaviour depends on what type of value the
option has. It could be eitherCallable[[Any], Any]orstr.A value of typeCallable[[Any], Any]is a generic way to specify any callable
object like a function, a class method, a class instance method, an instance
of a callable class or even a lambda function to be called for deserialization.A value of typestrsets a specific engine for deserialization. Keep in mind
that all possible engines depend on the data type that this option is used
with. At this moment there are next deserialization engines to choose from:Applicable data typesSupported enginesDescriptiondatetime,date,timeciso8601,pendulumHow to parse datetime string. By default nativefromisoformatof corresponding class will be used fordatetime,dateandtimefields. It's the fastest way in most cases, but you can choose an alternative.NamedTuple,namedtupleas_list,as_dictHow to unpack named tuples. By defaultas_listengine is used that means your named tuple class instance will be created from a list of its values. You can unpack it from a dictionary usingas_dictengine.[!TIP]You can pass a field value as is without changes on deserialization usingpass_through.Example:fromdatetimeimportdatetimefromdataclassesimportdataclass,fieldfromtypingimportList,NamedTuplefrommashumaroimportDataClassDictMixinimportciso8601importdateutilclassMyNamedTuple(NamedTuple):x:inty:float@dataclassclassA(DataClassDictMixin):x:datetime=field(metadata={"deserialize":"pendulum"})classB(DataClassDictMixin):x:datetime=field(metadata={"deserialize":ciso8601.parse_datetime_as_naive})@dataclassclassC(DataClassDictMixin):dt:List[datetime]=field(metadata={"deserialize":lambdal:list(map(dateutil.parser.isoparse,l))})@dataclassclassD(DataClassDictMixin):x:MyNamedTuple=field(metadata={"deserialize":"as_dict"})serialization_strategyoptionThis option is useful when you want to change the serialization logic
for a dataclass field depending on some defined parameters using a reusable
serialization scheme. You can find an example in theSerializationStrategychapter.[!TIP]You can pass a field value as is without changes on
serialization / deserialization usingpass_through.aliasoptionIn some cases it's better to have different names for a field in your class and
in its serialized view. For example, a third-party legacy API you are working
with might operate with camel case style, but you stick to snake case style in
your code base. Or even you want to load data with keys that are invalid
identifiers in Python. This problem is easily solved by using aliases:fromdataclassesimportdataclass,fieldfrommashumaroimportDataClassDictMixin,field_options@dataclassclassDataClass(DataClassDictMixin):a:int=field(metadata=field_options(alias="FieldA"))b:int=field(metadata=field_options(alias="#invalid"))x=DataClass.from_dict({"FieldA":1,"#invalid":2})# DataClass(a=1, b=2)x.to_dict()# {"a": 1, "b": 2}  # no aliases on serialization by defaultIf you want to write all the field aliases in one place, there isa config optionfor that.If you want to deserialize all the fields by its names along with aliases,
there isa config optionfor that.If you want to serialize all the fields by aliases you have two options to do so:serialize_by_aliasconfig optionserialize_by_aliasdialect optionby_aliaskeyword argument into_*methodsIt's hard to imagine when it might be necessary to serialize only specific
fields by alias, but such functionality is easily added to the library. Open
the issue if you need it.Config optionsIf inheritance is not an empty word for you, you'll fall in love with theConfigclass. You can registerserializeanddeserializemethods, define
code generation options and other things just in one place. Or in some
classes in different ways if you need flexibility. Inheritance is always on the
first place.There is a base classBaseConfigthat you can inherit for the sake of
convenience, but it's not mandatory.In the following example you can see how
thedebugflag is changed from class to class:ModelAwill have debug mode enabled butModelBwill not.frommashumaroimportDataClassDictMixinfrommashumaro.configimportBaseConfigclassBaseModel(DataClassDictMixin):classConfig(BaseConfig):debug=TrueclassModelA(BaseModel):a:intclassModelB(BaseModel):b:intclassConfig(BaseConfig):debug=FalseNext section describes all supported options to use in the config.debugconfig optionIf you enable thedebugoption the generated code for your data class
will be printed.code_generation_optionsconfig optionSome users may need functionality that wouldn't exist without extra cost such
as valuable cpu time to execute additional instructions. Since not everyone
needs such instructions, they can be enabled by a constant in the list,
so the fastest basic behavior of the library will always remain by default.
The following table provides a brief overview of all the available constants
described below.ConstantDescriptionTO_DICT_ADD_OMIT_NONE_FLAGAddsomit_nonekeyword-only argument toto_*methods.TO_DICT_ADD_BY_ALIAS_FLAGAddsby_aliaskeyword-only argument toto_*methods.ADD_DIALECT_SUPPORTAddsdialectkeyword-only argument tofrom_*andto_*methods.ADD_SERIALIZATION_CONTEXTAddscontextkeyword-only argument toto_*methods.serialization_strategyconfig optionYou can register customSerializationStrategy,serializeanddeserializemethods for specific types just in one place. It could be configured using
a dictionary with types as keys. The value could be either aSerializationStrategyinstance or a dictionary withserializeanddeserializevalues with the same meaning as in thefield options.fromdataclassesimportdataclassfromdatetimeimportdatetime,datefrommashumaroimportDataClassDictMixinfrommashumaro.configimportBaseConfigfrommashumaro.typesimportSerializationStrategyclassFormattedDateTime(SerializationStrategy):def__init__(self,fmt):self.fmt=fmtdefserialize(self,value:datetime)->str:returnvalue.strftime(self.fmt)defdeserialize(self,value:str)->datetime:returndatetime.strptime(value,self.fmt)@dataclassclassDataClass(DataClassDictMixin):x:datetimey:dateclassConfig(BaseConfig):serialization_strategy={datetime:FormattedDateTime("%Y"),date:{# you can use specific str values for datetime here as well"deserialize":"pendulum","serialize":date.isoformat,},}instance=DataClass.from_dict({"x":"2021","y":"2021"})# DataClass(x=datetime.datetime(2021, 1, 1, 0, 0), y=Date(2021, 1, 1))dictionary=instance.to_dict()# {'x': '2021', 'y': '2021-01-01'}Note that you can register different methods for multiple logical types which
are based on the same type usingNewTypeandAnnotated,
seeExtending existing typesfor details.It's also possible to define a generic (de)serialization method for a generic
type by registering a method for itsorigintype.
Although this technique is widely used when working withthird-party generic
typesusing generic strategies, it can also be
applied in simple scenarios:fromdataclassesimportdataclassfrommashumaroimportDataClassDictMixin@dataclassclassC(DataClassDictMixin):ints:list[int]floats:list[float]classConfig:serialization_strategy={list:{# origin type for list[int] and list[float] is list"serialize":lambdax:list(map(str,x)),}}assertC([1],[2.2]).to_dict()=={'ints':['1'],'floats':['2.2']}aliasesconfig optionSometimes it's better to write the field aliases in one place. You can mix
aliases here withaliases in the field options, but the last ones will always
take precedence.fromdataclassesimportdataclassfrommashumaroimportDataClassDictMixinfrommashumaro.configimportBaseConfig@dataclassclassDataClass(DataClassDictMixin):a:intb:intclassConfig(BaseConfig):aliases={"a":"FieldA","b":"FieldB",}DataClass.from_dict({"FieldA":1,"FieldB":2})# DataClass(a=1, b=2)serialize_by_aliasconfig optionAll the fields withaliaseswill be serialized by them by
default when this option is enabled. You can mix this config option withby_aliaskeyword argument.fromdataclassesimportdataclass,fieldfrommashumaroimportDataClassDictMixin,field_optionsfrommashumaro.configimportBaseConfig@dataclassclassDataClass(DataClassDictMixin):field_a:int=field(metadata=field_options(alias="FieldA"))classConfig(BaseConfig):serialize_by_alias=TrueDataClass(field_a=1).to_dict()# {'FieldA': 1}allow_deserialization_not_by_aliasconfig optionWhen using aliases, the deserializer defaults to requiring the keys to match
what is defined as the alias.
If the flexibility to deserialize aliased and unaliased keys is required then
the config optionallow_deserialization_not_by_aliascan be set to
enable the feature.fromdataclassesimportdataclass,fieldfrommashumaroimportDataClassDictMixinfrommashumaro.configimportBaseConfig@dataclassclassAliasedDataClass(DataClassDictMixin):foo:int=field(metadata={"alias":"alias_foo"})bar:int=field(metadata={"alias":"alias_bar"})classConfig(BaseConfig):allow_deserialization_not_by_alias=Truealias_dict={"alias_foo":1,"alias_bar":2}t1=AliasedDataClass.from_dict(alias_dict)no_alias_dict={"foo":1,"bar":2}# This would raise `mashumaro.exceptions.MissingField`# if allow_deserialization_not_by_alias was Falset2=AliasedDataClass.from_dict(no_alias_dict)assertt1==t2omit_noneconfig optionAll the fields withNonevalues will be skipped during serialization by
default when this option is enabled. You can mix this config option withomit_nonekeyword argument.fromdataclassesimportdataclassfromtypingimportOptionalfrommashumaroimportDataClassDictMixinfrommashumaro.configimportBaseConfig@dataclassclassDataClass(DataClassDictMixin):x:Optional[int]=42classConfig(BaseConfig):omit_none=TrueDataClass(x=None).to_dict()# {}omit_defaultconfig optionWhen this option enabled, all the fields that have values equal to the defaults
or the default_factory results will be skipped during serialization.fromdataclassesimportdataclass,fieldfromtypingimportList,Optional,TuplefrommashumaroimportDataClassDictMixin,field_optionsfrommashumaro.configimportBaseConfig@dataclassclassFoo:foo:str@dataclassclassDataClass(DataClassDictMixin):a:int=42b:Tuple[int,...]=field(default=(1,2,3))c:List[Foo]=field(default_factory=lambda:[Foo("foo")])d:Optional[str]=NoneclassConfig(BaseConfig):omit_default=TrueDataClass(a=42,b=(1,2,3),c=[Foo("foo")]).to_dict()# {}namedtuple_as_dictconfig optionDataclasses are a great way to declare and use data models. But it's not the only way.
Python has a typed version ofnamedtuplecalledNamedTuplewhich looks similar to dataclasses:fromtypingimportNamedTupleclassPoint(NamedTuple):x:inty:intthe same with a dataclass will look like this:fromdataclassesimportdataclass@dataclassclassPoint:x:inty:intAt first glance, you can use both options. But imagine that you need to create
a bunch of instances of thePointclass. Due to how dataclasses work you will
have more memory consumption compared to named tuples. In such a case it could
be more appropriate to use named tuples.By default, all named tuples are packed into lists. But withnamedtuple_as_dictoption you have a drop-in replacement for dataclasses:fromdataclassesimportdataclassfromtypingimportList,NamedTuplefrommashumaroimportDataClassDictMixinclassPoint(NamedTuple):x:inty:int@dataclassclassDataClass(DataClassDictMixin):points:List[Point]classConfig:namedtuple_as_dict=Trueobj=DataClass.from_dict({"points":[{"x":0,"y":0},{"x":1,"y":1}]})print(obj.to_dict())# {"points": [{"x": 0, "y": 0}, {"x": 1, "y": 1}]}If you want to serialize only certain named tuple fields as dictionaries, you
can use the correspondingserializationanddeserializationengines.allow_postponed_evaluationconfig optionPEP 563solved the problem of forward references by postponing the evaluation
of annotations, so you can write the following code:from__future__importannotationsfromdataclassesimportdataclassfrommashumaroimportDataClassDictMixin@dataclassclassA(DataClassDictMixin):x:B@dataclassclassB(DataClassDictMixin):y:intobj=A.from_dict({'x':{'y':1}})You don't need to write anything special here, forward references work out of
the box. If a field of a dataclass has a forward reference in the type
annotations, building offrom_*andto_*methods of this dataclass
will be postponed until they are called once. However, if for some reason you
don't want the evaluation to be possibly postponed, you can disable it usingallow_postponed_evaluationoption:from__future__importannotationsfromdataclassesimportdataclassfrommashumaroimportDataClassDictMixin@dataclassclassA(DataClassDictMixin):x:BclassConfig:allow_postponed_evaluation=False# UnresolvedTypeReferenceError: Class A has unresolved type reference B# in some of its fields@dataclassclassB(DataClassDictMixin):y:intIn this case you will getUnresolvedTypeReferenceErrorregardless of whether
class B is declared below or not.dialectconfig optionThis option is describedbelowin the
Dialects section.orjson_optionsconfig optionThis option changes default options fororjson.dumpsencoder which is
used inDataClassORJSONMixin. For example, you can
tell orjson to handle non-strdictkeys as the built-injson.dumpsencoder does. Seeorjson documentationto read more about these options.importorjsonfromdataclassesimportdataclassfromtypingimportDictfrommashumaro.configimportBaseConfigfrommashumaro.mixins.orjsonimportDataClassORJSONMixin@dataclassclassMyClass(DataClassORJSONMixin):x:Dict[int,int]classConfig(BaseConfig):orjson_options=orjson.OPT_NON_STR_KEYSassertMyClass({1:2}).to_json()=={"1":2}discriminatorconfig optionThis option is described in theClass level discriminatorsection.lazy_compilationconfig optionBy using this option, the compilation of thefrom_*andto_*methods will
be deferred until they are called first time. This will reduce the import time
and, in certain instances, may enhance the speed of deserialization
by leveraging the data that is accessible after the class has been created.[!CAUTION]If you need to save a reference tofrom_*orto_*method, you should
do it after the method is compiled. To be safe, you can always use lambda
function:from_dict=lambdax:MyModel.from_dict(x)to_dict=lambdax:x.to_dict()sort_keysconfig optionWhen set, the keys on serialized dataclasses will be sorted in alphabetical order.Unlike thesort_keysoption in the standard library'sjson.dumpsfunction, this option acts at class creation time and has no effect on the performance of serialization.fromdataclassesimportdataclassfrommashumaroimportDataClassDictMixinfrommashumaro.configimportBaseConfig@dataclassclassSortedDataClass(DataClassDictMixin):foo:intbar:intclassConfig(BaseConfig):sort_keys=Truet=SortedDataClass(1,2)assertt.to_dict()=={"bar":2,"foo":1}Passing field values as isIn some cases it's needed to pass a field value as is without any changes
during serialization / deserialization. There is a predefinedpass_throughobject that can be used asserialization_strategyorserialize/deserializeoptions:fromdataclassesimportdataclass,fieldfrommashumaroimportDataClassDictMixin,pass_throughclassMyClass:def__init__(self,some_value):self.some_value=some_value@dataclassclassA1(DataClassDictMixin):x:MyClass=field(metadata={"serialize":pass_through,"deserialize":pass_through,})@dataclassclassA2(DataClassDictMixin):x:MyClass=field(metadata={"serialization_strategy":pass_through,})@dataclassclassA3(DataClassDictMixin):x:MyClassclassConfig:serialization_strategy={MyClass:pass_through,}@dataclassclassA4(DataClassDictMixin):x:MyClassclassConfig:serialization_strategy={MyClass:{"serialize":pass_through,"deserialize":pass_through,}}my_class_instance=MyClass(42)assertA1.from_dict({'x':my_class_instance}).x==my_class_instanceassertA2.from_dict({'x':my_class_instance}).x==my_class_instanceassertA3.from_dict({'x':my_class_instance}).x==my_class_instanceassertA4.from_dict({'x':my_class_instance}).x==my_class_instancea1_dict=A1(my_class_instance).to_dict()a2_dict=A2(my_class_instance).to_dict()a3_dict=A3(my_class_instance).to_dict()a4_dict=A4(my_class_instance).to_dict()asserta1_dict==a2_dict==a3_dict==a4_dict=={"x":my_class_instance}Extending existing typesThere are situations where you might want some values of the same type to be
treated as their own type. You can create new logical types withNewTypeorAnnotatedand register serialization strategies for them:fromtypingimportMapping,NewType,AnnotatedfromdataclassesimportdataclassfrommashumaroimportDataClassDictMixinSessionID=NewType("SessionID",str)AccountID=Annotated[str,"AccountID"]@dataclassclassContext(DataClassDictMixin):account_sessions:Mapping[AccountID,SessionID]classConfig:serialization_strategy={AccountID:{"deserialize":lambdax:...,"serialize":lambdax:...,},SessionID:{"deserialize":lambdax:...,"serialize":lambdax:...,}}Although usingNewTypeis usually the most reliable way to avoid logical
errors, you have to pay for it with notable overhead. If you are creating
dataclass instances manually, then you know that type checkers will
enforce you to enclose a value in your"NewType"callable, which leads
to performance degradation:python-mtimeit-s"from typing import NewType; MyInt = NewType('MyInt', int)""MyInt(42)"10000000loops,bestof5:31.1nsecperlooppython-mtimeit-s"from typing import NewType; MyInt = NewType('MyInt', int)""42"50000000loops,bestof5:4.35nsecperloopHowever, when you create dataclass instances using thefrom_*method provided
by one of the mixins or using one of the decoders, there will be no performance
degradation, because the value won't be enclosed in the callable in the
generated code. Therefore, if performance is more important to you than
catching logical errors by type checkers, and you are actively creating or
changing dataclasses manually, then you should take a closer look at usingAnnotated.DialectsSometimes it's needed to have different serialization and deserialization
methods depending on the data source where entities of the dataclass are
stored or on the API to which the entities are being sent or received from.
There is a specialDialecttype that may contain all the differences from the
default serialization and deserialization methods. You can create different
dialects and use each of them for the same dataclass depending on
the situation.Suppose we have the following dataclass with a field of typedate:@dataclassclassEntity(DataClassDictMixin):dt:dateBy default, a field ofdatetype serializes to a string in ISO 8601 format,
so the serialized entity will look like{'dt': '2021-12-31'}. But what if we
have, for example, two sensitive legacy Ethiopian and Japanese APIs that use
two different formats for dates —dd/mm/yyyyandyyyy年mm月dd日? Instead of
creating two similar dataclasses we can have one dataclass and two dialects:fromdataclassesimportdataclassfromdatetimeimportdate,datetimefrommashumaroimportDataClassDictMixinfrommashumaro.configimportADD_DIALECT_SUPPORTfrommashumaro.dialectimportDialectfrommashumaro.typesimportSerializationStrategyclassDateTimeSerializationStrategy(SerializationStrategy):def__init__(self,fmt:str):self.fmt=fmtdefserialize(self,value:date)->str:returnvalue.strftime(self.fmt)defdeserialize(self,value:str)->date:returndatetime.strptime(value,self.fmt).date()classEthiopianDialect(Dialect):serialization_strategy={date:DateTimeSerializationStrategy("%d/%m/%Y")}classJapaneseDialect(Dialect):serialization_strategy={date:DateTimeSerializationStrategy("%Y年%m月%d日")}@dataclassclassEntity(DataClassDictMixin):dt:dateclassConfig:code_generation_options=[ADD_DIALECT_SUPPORT]entity=Entity(date(2021,12,31))entity.to_dict(dialect=EthiopianDialect)# {'dt': '31/12/2021'}entity.to_dict(dialect=JapaneseDialect)# {'dt': '2021年12月31日'}Entity.from_dict({'dt':'2021年12月31日'},dialect=JapaneseDialect)serialization_strategydialect optionThis dialect option has the same meaning as thesimilar config optionbut for the dialect scope. You can register customSerializationStrategy,serializeanddeserializemethods for the specific types.serialize_by_aliasdialect optionThis dialect option has the same meaning as thesimilar config optionbut for the dialect scope.omit_nonedialect optionThis dialect option has the same meaning as thesimilar config optionbut for the dialect scope.omit_defaultdialect optionThis dialect option has the same meaning as thesimilar config optionbut for the dialect scope.namedtuple_as_dictdialect optionThis dialect option has the same meaning as thesimilar config optionbut for the dialect scope.no_copy_collectionsdialect optionBy default, all collection data types are serialized as a copy to prevent
mutation of the original collection. As an example, if a dataclass contains
a field of typelist[str], then it will be serialized as a copy of the
original list, so you can safely mutate it after. The downside is that copying
is always slower than using a reference to the original collection. In some
cases we know beforehand that mutation doesn't take place or is even desirable,
so we can benefit from avoiding unnecessary copies by settingno_copy_collectionsto a sequence of origin collection data types.
This is applicable only for collections containing elements that do not
require conversion.fromdataclassesimportdataclassfrommashumaroimportDataClassDictMixinfrommashumaro.configimportBaseConfigfrommashumaro.dialectimportDialectclassNoCopyDialect(Dialect):no_copy_collections=(list,dict,set)@dataclassclassDataClass(DataClassDictMixin):simple_list:list[str]simple_dict:dict[str,str]simple_set:set[str]classConfig(BaseConfig):dialect=NoCopyDialectobj=DataClass(["foo"],{"bar":"baz"},{"foobar"})data=obj.to_dict()assertdata["simple_list"]isobj.simple_listassertdata["simple_dict"]isobj.simple_dictassertdata["simple_set"]isobj.simple_setThis option is enabled forlistanddictin the default dialects that
belong to mixins and codecs for the following formats:JSON (orjson library)TOMLMessagePackChanging the default dialectYou can change the default serialization and deserialization methods not only
in theserialization_strategyconfig
option but also using thedialectconfig option. If you have multiple
dataclasses without a common parent class the default dialect can help you
to reduce the number of code lines written:@dataclassclassEntity(DataClassDictMixin):dt:dateclassConfig:dialect=JapaneseDialectentity=Entity(date(2021,12,31))entity.to_dict()# {'dt': '2021年12月31日'}assertEntity.from_dict({'dt':'2021年12月31日'})==entityDefault dialect can also be set when using codecs:frommashumaro.codecsimportBasicDecoder,BasicEncoder@dataclassclassEntity:dt:datedecoder=BasicDecoder(Entity,default_dialect=JapaneseDialect)encoder=BasicEncoder(Entity,default_dialect=JapaneseDialect)entity=Entity(date(2021,12,31))encoder.encode(entity)# {'dt': '2021年12月31日'}assertdecoder.decode({'dt':'2021年12月31日'})==entityDiscriminatorThere is a specialDiscriminatorclass that allows you to customize how
a union of dataclasses or their hierarchy will be deserialized.
It has the following parameters that affects class selection rules:field— optional name of the input dictionary key (also known as tag)
by which all the variants can be distinguishedinclude_subtypes— allow to deserialize subclassesinclude_supertypes— allow to deserialize superclassesvariant_tagger_fn— a custom function used to generate tag values
associated with a variantBy default, each variant that you want to discriminate by tags should have a
class-level attribute containing an associated tag value. This attribute should
have a name defined byfieldparameter. The tag value coule be in the
following forms:without annotations:type = 42annotated as ClassVar:type: ClassVar[int] = 42annotated as Final:type: Final[int] = 42annotated as Literal:type: Literal[42] = 42annotated as StrEnum:type: ResponseType = ResponseType.OK[!NOTE]Keep in mind that by default only Final, Literal and StrEnum fields are
processed during serialization.However, it is possible to use discriminator without the class-level
attribute. You can provide a custom function that generates one or many variant
tag values. This function should take a class as the only argument and return
either a single value of the basic type likestrorintor a list of them
to associate multiple tags with a variant. The common practice is to use
a class name as a single tag value:variant_tagger_fn=lambdacls:cls.__name__Next, we will look at different use cases, as well as their pros and cons.Subclasses distinguishable by a fieldOften you have a base dataclass and multiple subclasses that are easily
distinguishable from each other by the value of a particular field.
For example, there may be different events, messages or requests with
a discriminator field "event_type", "message_type" or just "type". You could've
listed all of them withinUniontype, but it would be too verbose and
impractical. Moreover, deserialization of the union would be slow, since we
need to iterate over each variant in the list until we find the right one.We can improve subclass deserialization usingDiscriminatoras annotation
withinAnnotatedtype. We will usefieldparameter and setinclude_subtypestoTrue.[!IMPORTANT]The discriminator field should be accessible from the__dict__attribute
of a specific descendant, i.e. defined at the level of that descendant.
A descendant class without a discriminator field will be ignored, but
its descendants won't.Suppose we have a hierarchy of client events distinguishable by a class
attribute "type":fromdataclassesimportdataclassfromipaddressimportIPv4AddressfrommashumaroimportDataClassDictMixin@dataclassclassClientEvent(DataClassDictMixin):pass@dataclassclassClientConnectedEvent(ClientEvent):type="connected"client_ip:IPv4Address@dataclassclassClientDisconnectedEvent(ClientEvent):type="disconnected"client_ip:IPv4AddressWe use base dataclassClientEventfor a field of another dataclass:fromtypingimportAnnotated,List# or from typing_extensions import Annotatedfrommashumaro.typesimportDiscriminator@dataclassclassAggregatedEvents(DataClassDictMixin):list:List[Annotated[ClientEvent,Discriminator(field="type",include_subtypes=True)]]Now we can deserialize events based on "type" value:events=AggregatedEvents.from_dict({"list":[{"type":"connected","client_ip":"10.0.0.42"},{"type":"disconnected","client_ip":"10.0.0.42"},]})assertevents==AggregatedEvents(list=[ClientConnectedEvent(client_ip=IPv4Address("10.0.0.42")),ClientDisconnectedEvent(client_ip=IPv4Address("10.0.0.42")),])Subclasses without a common fieldIn rare cases you have to deal with subclasses that don't have a common field
name which they can be distinguished by. SinceDiscriminatorcan be
initialized without "field" parameter you can use it with onlyinclude_subclassesenabled. The drawback is that we will have to go through all
the subclasses until we find the suitable one. It's almost like usingUniontype but with subclasses support.Suppose we're making a brunch. We have some ingredients:@dataclassclassIngredient(DataClassDictMixin):name:str@dataclassclassHummus(Ingredient):made_of:Literal["chickpeas","beet","artichoke"]grams:int@dataclassclassCelery(Ingredient):pieces:intLet's create a plate:@dataclassclassPlate(DataClassDictMixin):ingredients:List[Annotated[Ingredient,Discriminator(include_subtypes=True)]]And now we can put our ingredients on the plate:plate=Plate.from_dict({"ingredients":[{"name":"hummus from the shop","made_of":"chickpeas","grams":150,},{"name":"celery from my garden","pieces":5},]})assertplate==Plate(ingredients=[Hummus(name="hummus from the shop",made_of="chickpeas",grams=150),Celery(name="celery from my garden",pieces=5),])In some cases it's necessary to fall back to the base class if there is no
suitable subclass. We can setinclude_supertypestoTrue:@dataclassclassPlate(DataClassDictMixin):ingredients:List[Annotated[Ingredient,Discriminator(include_subtypes=True,include_supertypes=True),]]plate=Plate.from_dict({"ingredients":[{"name":"hummus from the shop","made_of":"chickpeas","grams":150,},{"name":"celery from my garden","pieces":5},{"name":"cumin"}# <- new unknown ingredient]})assertplate==Plate(ingredients=[Hummus(name="hummus from the shop",made_of="chickpeas",grams=150),Celery(name="celery from my garden",pieces=5),Ingredient(name="cumin"),# <- unknown ingredient added])Class level discriminatorIt may often be more convenient to specify aDiscriminatoronce at the class
level and use that class withoutAnnotatedtype for subclass deserialization.
Depending on theDiscriminatorparameters, it can be used as a replacement forsubclasses distinguishable by a fieldas well as forsubclasses without a common field.
The only difference is that you can't useinclude_supertypes=Truebecause
it would lead to a recursion error.Reworked example will look like this:fromdataclassesimportdataclassfromipaddressimportIPv4AddressfromtypingimportListfrommashumaroimportDataClassDictMixinfrommashumaro.configimportBaseConfigfrommashumaro.typesimportDiscriminator@dataclassclassClientEvent(DataClassDictMixin):classConfig(BaseConfig):discriminator=Discriminator(# <- add discriminatorfield="type",include_subtypes=True,)@dataclassclassClientConnectedEvent(ClientEvent):type="connected"client_ip:IPv4Address@dataclassclassClientDisconnectedEvent(ClientEvent):type="disconnected"client_ip:IPv4Address@dataclassclassAggregatedEvents(DataClassDictMixin):list:List[ClientEvent]# <- use base class hereAnd now we can deserialize events based on "type" value as we did earlier:events=AggregatedEvents.from_dict({"list":[{"type":"connected","client_ip":"10.0.0.42"},{"type":"disconnected","client_ip":"10.0.0.42"},]})assertevents==AggregatedEvents(list=[ClientConnectedEvent(client_ip=IPv4Address("10.0.0.42")),ClientDisconnectedEvent(client_ip=IPv4Address("10.0.0.42")),])What's more interesting is that you can now deserialize subclasses simply by
calling the superclassfrom_*method, which is very useful:disconnected_event=ClientEvent.from_dict({"type":"disconnected","client_ip":"10.0.0.42"})assertdisconnected_event==ClientDisconnectedEvent(IPv4Address("10.0.0.42"))The same is applicable for subclasses without a common field:@dataclassclassIngredient(DataClassDictMixin):name:strclassConfig:discriminator=Discriminator(include_subtypes=True)...celery=Ingredient.from_dict({"name":"celery from my garden","pieces":5})assertcelery==Celery(name="celery from my garden",pieces=5)Working with union of classesDeserialization of union of types distinguishable by a particular field will
be much faster usingDiscriminatorbecause there will be no traversal
of all classes and an attempt to deserialize each of them.
Usually this approach can be used when you have multiple classes without a
common superclass or when you only need to deserialize some of the subclasses.
In the following example we will useinclude_supertypes=Trueto
deserialize two subclasses out of three:fromdataclassesimportdataclassfromtypingimportAnnotated,Literal,Union# or from typing_extensions import AnnotatedfrommashumaroimportDataClassDictMixinfrommashumaro.typesimportDiscriminator@dataclassclassEvent(DataClassDictMixin):pass@dataclassclassEvent1(Event):code:Literal[1]=1...@dataclassclassEvent2(Event):code:Literal[2]=2...@dataclassclassEvent3(Event):code:Literal[3]=3...@dataclassclassMessage(DataClassDictMixin):event:Annotated[Union[Event1,Event2],Discriminator(field="code",include_supertypes=True),]event1_msg=Message.from_dict({"event":{"code":1,...}})event2_msg=Message.from_dict({"event":{"code":2,...}})assertisinstance(event1_msg.event,Event1)assertisinstance(event2_msg.event,Event2)# raises InvalidFieldValue:Message.from_dict({"event":{"code":3,...}})Again, it's not necessary to have a common superclass. If you have a union of
dataclasses without a field that they can be distinguishable by, you can still
useDiscriminator, but deserialization will almost be the same as forUniontype withoutDiscriminatorexcept that it could be possible to deserialize
subclasses withinclude_subtypes=True.[!IMPORTANT]When bothinclude_subtypesandinclude_supertypesare enabled,
all subclasses will be attempted to be deserialized first,
superclasses — at the end.In the following example you can see how priority works — first we try
to deserializeChickpeaHummus, and if it fails, then we tryHummus:@dataclassclassHummus(DataClassDictMixin):made_of:Literal["chickpeas","artichoke"]grams:int@dataclassclassChickpeaHummus(Hummus):made_of:Literal["chickpeas"]@dataclassclassCelery(DataClassDictMixin):pieces:int@dataclassclassPlate(DataClassDictMixin):ingredients:List[Annotated[Union[Hummus,Celery],Discriminator(include_subtypes=True,include_supertypes=True),]]plate=Plate.from_dict({"ingredients":[{"made_of":"chickpeas","grams":100},{"made_of":"artichoke","grams":50},{"pieces":4},]})assertplate==Plate(ingredients=[ChickpeaHummus(made_of='chickpeas',grams=100),# <- subclassHummus(made_of='artichoke',grams=50),# <- superclassCelery(pieces=4),])Using a custom variant tagger functionSometimes it is impractical to have a class-level attribute with a tag value,
especially when you have a lot of classes. We can have a custom tagger
function instead. This method is applicable for all scenarios of using
the discriminator, but for demonstration purposes, let's focus only on one
of them.Suppose we want to use the middle part ofClient*Eventas a tag value:fromdataclassesimportdataclassfromipaddressimportIPv4AddressfrommashumaroimportDataClassDictMixinfrommashumaro.configimportBaseConfigfrommashumaro.typesimportDiscriminatordefclient_event_tagger(cls):# not the best way of doing it, it's just a demoreturncls.__name__[6:-5].lower()@dataclassclassClientEvent(DataClassDictMixin):classConfig(BaseConfig):discriminator=Discriminator(field="type",include_subtypes=True,variant_tagger_fn=client_event_tagger,)@dataclassclassClientConnectedEvent(ClientEvent):client_ip:IPv4Address@dataclassclassClientDisconnectedEvent(ClientEvent):client_ip:IPv4AddressWe can now deserialize subclasses as we did it earlierwithout variant tagger:disconnected_event=ClientEvent.from_dict({"type":"disconnected","client_ip":"10.0.0.42"})assertdisconnected_event==ClientDisconnectedEvent(IPv4Address("10.0.0.42"))If we need to associate multiple tags with a single variant, we can return
a list of tags:defclient_event_tagger(cls):name=cls.__name__[6:-5]return[name.lower(),name.upper()]Code generation optionsAddomit_nonekeyword argumentIf you want to have control over whether to skipNonevalues on serialization
you can addomit_noneparameter toto_*methods using thecode_generation_optionslist. The default value ofomit_noneparameter depends on whether theomit_noneconfig option oromit_nonedialect option is enabled.fromdataclassesimportdataclassfrommashumaroimportDataClassDictMixinfrommashumaro.configimportBaseConfig,TO_DICT_ADD_OMIT_NONE_FLAG@dataclassclassInner(DataClassDictMixin):x:int=None# "x" won't be omitted since there is no TO_DICT_ADD_OMIT_NONE_FLAG here@dataclassclassModel(DataClassDictMixin):x:Innera:int=Noneb:str=None# will be omittedclassConfig(BaseConfig):code_generation_options=[TO_DICT_ADD_OMIT_NONE_FLAG]Model(x=Inner(),a=1).to_dict(omit_none=True)# {'x': {'x': None}, 'a': 1}Addby_aliaskeyword argumentIf you want to have control over whether to serialize fields by theiraliasesyou can addby_aliasparameter toto_*methods
using thecode_generation_optionslist. The default value ofby_aliasparameter depends on whether theserialize_by_aliasconfig option is enabled.fromdataclassesimportdataclass,fieldfrommashumaroimportDataClassDictMixin,field_optionsfrommashumaro.configimportBaseConfig,TO_DICT_ADD_BY_ALIAS_FLAG@dataclassclassDataClass(DataClassDictMixin):field_a:int=field(metadata=field_options(alias="FieldA"))classConfig(BaseConfig):code_generation_options=[TO_DICT_ADD_BY_ALIAS_FLAG]DataClass(field_a=1).to_dict()# {'field_a': 1}DataClass(field_a=1).to_dict(by_alias=True)# {'FieldA': 1}Adddialectkeyword argumentSupport fordialectsis disabled by default for performance reasons. You can enable
it using aADD_DIALECT_SUPPORTconstant:fromdataclassesimportdataclassfromdatetimeimportdatefrommashumaroimportDataClassDictMixinfrommashumaro.configimportBaseConfig,ADD_DIALECT_SUPPORT@dataclassclassEntity(DataClassDictMixin):dt:dateclassConfig(BaseConfig):code_generation_options=[ADD_DIALECT_SUPPORT]Addcontextkeyword argumentSometimes it's needed to pass a "context" object to the serialization hooks
that will take it into account. For example, you could want to have an option
to remove sensitive data from the serialization result if you need to.
You can addcontextparameter toto_*methods that will be passed to__pre_serialize__and__post_serialize__hooks. The type of this context
as well as its mutability is up to you.fromdataclassesimportdataclassfromtypingimportDict,OptionalfromuuidimportUUIDfrommashumaroimportDataClassDictMixinfrommashumaro.configimportBaseConfig,ADD_SERIALIZATION_CONTEXTclassBaseModel(DataClassDictMixin):classConfig(BaseConfig):code_generation_options=[ADD_SERIALIZATION_CONTEXT]@dataclassclassAccount(BaseModel):id:UUIDusername:strname:strdef__pre_serialize__(self,context:Optional[Dict]=None):returnselfdef__post_serialize__(self,d:Dict,context:Optional[Dict]=None):ifcontextandcontext.get("remove_sensitive_data"):d["username"]="***"d["name"]="***"returnd@dataclassclassSession(BaseModel):id:UUIDkey:straccount:Accountdef__pre_serialize__(self,context:Optional[Dict]=None):returnselfdef__post_serialize__(self,d:Dict,context:Optional[Dict]=None):ifcontextandcontext.get("remove_sensitive_data"):d["key"]="***"returndfoo=Session(id=UUID('03321c9f-6a97-421e-9869-918ff2867a71'),key="VQ6Q9bX4c8s",account=Account(id=UUID('4ef2baa7-edef-4d6a-b496-71e6d72c58fb'),username="john_doe",name="John"))assertfoo.to_dict()=={'id':'03321c9f-6a97-421e-9869-918ff2867a71','key':'VQ6Q9bX4c8s','account':{'id':'4ef2baa7-edef-4d6a-b496-71e6d72c58fb','username':'john_doe','name':'John'}}assertfoo.to_dict(context={"remove_sensitive_data":True})=={'id':'03321c9f-6a97-421e-9869-918ff2867a71','key':'***','account':{'id':'4ef2baa7-edef-4d6a-b496-71e6d72c58fb','username':'***','name':'***'}}Generic dataclassesAlong withuser-defined generic typesimplementingSerializableTypeinterface, generic and variadic
generic dataclasses can also be used. There are two applicable scenarios
for them.Generic dataclass inheritanceIf you have a generic dataclass and want to serialize and deserialize its
instances depending on the concrete types, you can use inheritance for that:fromdataclassesimportdataclassfromdatetimeimportdatefromtypingimportGeneric,Mapping,TypeVar,TypeVarTuplefrommashumaroimportDataClassDictMixinKT=TypeVar("KT")VT=TypeVar("VT",date,str)Ts=TypeVarTuple("Ts")@dataclassclassGenericDataClass(Generic[KT,VT,*Ts]):x:Mapping[KT,VT]y:Tuple[*Ts,KT]@dataclassclassConcreteDataClass(GenericDataClass[str,date,*Tuple[float,...]],DataClassDictMixin,):passConcreteDataClass.from_dict({"x":{"a":"2021-01-01"},"y":[1,2,"a"]})# ConcreteDataClass(x={'a': datetime.date(2021, 1, 1)}, y=(1.0, 2.0, 'a'))You can overrideTypeVarfield with a concrete type or anotherTypeVar.
Partial specification of concrete types is also allowed. If a generic dataclass
is inherited without type overriding the types of its fields remain untouched.Generic dataclass in a field typeAnother approach is to specify concrete types in the field type hints. This can
help to have different versions of the same generic dataclass:fromdataclassesimportdataclassfromdatetimeimportdatefromtypingimportGeneric,TypeVarfrommashumaroimportDataClassDictMixinT=TypeVar('T')@dataclassclassGenericDataClass(Generic[T],DataClassDictMixin):x:T@dataclassclassDataClass(DataClassDictMixin):date:GenericDataClass[date]str:GenericDataClass[str]instance=DataClass(date=GenericDataClass(x=date(2021,1,1)),str=GenericDataClass(x='2021-01-01'),)dictionary={'date':{'x':'2021-01-01'},'str':{'x':'2021-01-01'}}assertDataClass.from_dict(dictionary)==instanceGenericSerializableType interfaceThere is a generic alternative toSerializableTypecalledGenericSerializableType. It makes it possible to decide yourself how
to serialize and deserialize input data depending on the types provided:fromdataclassesimportdataclassfromdatetimeimportdatefromtypingimportDict,TypeVarfrommashumaroimportDataClassDictMixinfrommashumaro.typesimportGenericSerializableTypeKT=TypeVar("KT")VT=TypeVar("VT")classDictWrapper(Dict[KT,VT],GenericSerializableType):__packers__={date:lambdax:x.isoformat(),str:str}__unpackers__={date:date.fromisoformat,str:str}def_serialize(self,types)->Dict[KT,VT]:k_type,v_type=typesk_conv=self.__packers__[k_type]v_conv=self.__packers__[v_type]return{k_conv(k):v_conv(v)fork,vinself.items()}@classmethoddef_deserialize(cls,value,types)->"DictWrapper[KT, VT]":k_type,v_type=typesk_conv=cls.__unpackers__[k_type]v_conv=cls.__unpackers__[v_type]returncls({k_conv(k):v_conv(v)fork,vinvalue.items()})@dataclassclassDataClass(DataClassDictMixin):x:DictWrapper[date,str]y:DictWrapper[str,date]input_data={"x":{"2022-12-07":"2022-12-07"},"y":{"2022-12-07":"2022-12-07"},}obj=DataClass.from_dict(input_data)assertobj==DataClass(x=DictWrapper({date(2022,12,7):"2022-12-07"}),y=DictWrapper({"2022-12-07":date(2022,12,7)}),)assertobj.to_dict()==input_dataAs you can see, the code turns out to be massive compared to thealternativebut in rare cases such flexibility
can be useful. You should think twice about whether it's really worth using it.Serialization hooksIn some cases you need to prepare input / output data or do some extraordinary
actions at different stages of the deserialization / serialization lifecycle.
You can do this with different types of hooks.Before deserializationFor doing something with a dictionary that will be passed to deserialization
you can use__pre_deserialize__class method:@dataclassclassA(DataClassJSONMixin):abc:int@classmethoddef__pre_deserialize__(cls,d:Dict[Any,Any])->Dict[Any,Any]:return{k.lower():vfork,vind.items()}print(DataClass.from_dict({"ABC":123}))# DataClass(abc=123)print(DataClass.from_json('{"ABC": 123}'))# DataClass(abc=123)After deserializationFor doing something with a dataclass instance that was created as a result
of deserialization you can use__post_deserialize__class method:@dataclassclassA(DataClassJSONMixin):abc:int@classmethoddef__post_deserialize__(cls,obj:'A')->'A':obj.abc=456returnobjprint(DataClass.from_dict({"abc":123}))# DataClass(abc=456)print(DataClass.from_json('{"abc": 123}'))# DataClass(abc=456)Before serializationFor doing something before serialization you can use__pre_serialize__method:@dataclassclassA(DataClassJSONMixin):abc:intcounter:ClassVar[int]=0def__pre_serialize__(self)->'A':self.counter+=1returnselfobj=DataClass(abc=123)obj.to_dict()obj.to_json()print(obj.counter)# 2Note that you can add an additionalcontextargument using thecorrespondingcode generation option.After serializationFor doing something with a dictionary that was created as a result of
serialization you can use__post_serialize__method:@dataclassclassA(DataClassJSONMixin):user:strpassword:strdef__post_serialize__(self,d:Dict[Any,Any])->Dict[Any,Any]:d.pop('password')returndobj=DataClass(user="name",password="secret")print(obj.to_dict())# {"user": "name"}print(obj.to_json())# '{"user": "name"}'Note that you can add an additionalcontextargument using thecorrespondingcode generation option.JSON SchemaYou can build JSON Schema not only for dataclasses but also for any othersupporteddata
types. There is support for the following standards:Draft 2022-12OpenAPI Specification 3.1.0Building JSON SchemaFor simple one-time cases it's recommended to start from using a configurablebuild_json_schemafunction. It returnsJSONSchemaobject that can be
serialized to json or to dict:fromdataclassesimportdataclass,fieldfromtypingimportListfromuuidimportUUIDfrommashumaro.jsonschemaimportbuild_json_schema@dataclassclassUser:id:UUIDname:str=field(metadata={"description":"User name"})print(build_json_schema(List[User]).to_json())Click to show the result{"type":"array","items":{"type":"object","title":"User","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","description":"User name"}},"additionalProperties":false,"required":["id","name"]}}Additional validation keywords (see below)
can be added using annotations:fromtypingimportAnnotated,Listfrommashumaro.jsonschemaimportbuild_json_schemafrommashumaro.jsonschema.annotationsimportMaximum,MaxItemsprint(build_json_schema(Annotated[List[Annotated[int,Maximum(42)]],MaxItems(4)]).to_json())Click to show the result{"type":"array","items":{"type":"integer","maximum":42},"maxItems":4}The$schemakeyword can be added by settingwith_dialect_urito True:print(build_json_schema(str,with_dialect_uri=True).to_json())Click to show the result{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"string"}By default, Draft 2022-12 dialect is being used, but you can change it to
another one by settingdialectparameter:frommashumaro.jsonschemaimportOPEN_API_3_1print(build_json_schema(str,dialect=OPEN_API_3_1,with_dialect_uri=True).to_json())Click to show the result{"$schema":"https://spec.openapis.org/oas/3.1/dialect/base","type":"string"}All dataclass JSON Schemas can or can not be placed in thedefinitionssection, depending on theall_refsparameter, which default value comes
from a dialect used (Falsefor Draft 2022-12,Truefor OpenAPI
Specification 3.1.0):print(build_json_schema(List[User],all_refs=True).to_json())Click to show the result{"type":"array","$defs":{"User":{"type":"object","title":"User","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}},"additionalProperties":false,"required":["id","name"]}},"items":{"$ref":"#/$defs/User"}}The definitions section can be omitted from the final document by settingwith_definitionsparameter toFalse:print(build_json_schema(List[User],dialect=OPEN_API_3_1,with_definitions=False).to_json())Click to show the result{"type":"array","items":{"$ref":"#/components/schemas/User"}}Reference prefix can be changed by usingref_prefixparameter:print(build_json_schema(List[User],all_refs=True,with_definitions=False,ref_prefix="#/components/responses",).to_json())Click to show the result{"type":"array","items":{"$ref":"#/components/responses/User"}}The omitted definitions could be found later in theContextobject that
you could have created and passed to the function, but it could be easier
to useJSONSchemaBuilderfor that. For example, you might found it handy
to build OpenAPI Specification step by step passing your models to the builder
and get all the registered definitions later. This builder has reasonable
defaults but can be customized if necessary.frommashumaro.jsonschemaimportJSONSchemaBuilder,OPEN_API_3_1builder=JSONSchemaBuilder(OPEN_API_3_1)@dataclassclassUser:id:UUIDname:str@dataclassclassDevice:id:UUIDmodel:strprint(builder.build(List[User]).to_json())print(builder.build(List[Device]).to_json())print(builder.get_definitions().to_json())Click to show the result{"type":"array","items":{"$ref":"#/components/schemas/User"}}{"type":"array","items":{"$ref":"#/components/schemas/Device"}}{"User":{"type":"object","title":"User","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}},"additionalProperties":false,"required":["id","name"]},"Device":{"type":"object","title":"Device","properties":{"id":{"type":"string","format":"uuid"},"model":{"type":"string"}},"additionalProperties":false,"required":["id","model"]}}JSON Schema constraintsApart from required keywords, that are added automatically for certain data
types, you're free to use additional validation keywords.
They're presented by the corresponding classes inmashumaro.jsonschema.annotations:Number constraints:MinimumMaximumExclusiveMinimumExclusiveMaximumMultipleOfString constraints:MinLengthMaxLengthPatternArray constraints:MinItemsMaxItemsUniqueItemsContainsMinContainsMaxContainsObject constraints:MaxPropertiesMinPropertiesDependentRequiredExtending JSON SchemaUsing aConfigclass it is possible to override some parts of the schema.
Currently, you can do the following:override some field schemas using the "properties" keychangeadditionalPropertiesusing the "additionalProperties" keyfromdataclassesimportdataclassfrommashumaro.jsonschemaimportbuild_json_schema@dataclassclassFooBar:foo:strbar:intclassConfig:json_schema={"properties":{"foo":{"type":"string","description":"bar"}},"additionalProperties":True,}print(build_json_schema(FooBar).to_json())Click to show the result{"type":"object","title":"FooBar","properties":{"foo":{"type":"string","description":"bar"},"bar":{"type":"integer"}},"additionalProperties":true,"required":["foo","bar"]}You can also change the "additionalProperties" key to a specific schema
by passing it aJSONSchemainstance instead of a bool value.JSON Schema and custom serialization methodsMashumaro provides different ways to override default serialization methods for
dataclass fields or specific data types. In order for these overrides to be
reflected in the schema, you need to make sure that the methods have
annotations of the return value type.fromdataclassesimportdataclass,fieldfrommashumaro.configimportBaseConfigfrommashumaro.jsonschemaimportbuild_json_schemadefstr_as_list(s:str)->list[str]:returnlist(s)defint_as_str(i:int)->str:returnstr(i)@dataclassclassFooBar:foo:str=field(metadata={"serialize":str_as_list})bar:intclassConfig(BaseConfig):serialization_strategy={int:{"serialize":int_as_str}}print(build_json_schema(FooBar).to_json())Click to show the result{"type":"object","title":"FooBar","properties":{"foo":{"type":"array","items":{"type":"string"}},"bar":{"type":"string"}},"additionalProperties":false,"required":["foo","bar"]}