Package: nvelope
nvelopeDefine your JSON schema as Python dataclassesIt's kinda like Pydantic but better.Now with JSON-schema generation!Installationpip install nvelopeThe problem it solvesWithnvelopeyou can define dataclasses which know how to convert themselves from/to JSON.
All with custom checks and custom defined conversions from/to JSON for any type you want to put into your dataclass.This library was designed with extensibility in mind,
so it relies on interfaces (for the most part) rather than
some weird inheritance stuff and other magic.You can (and probably should) take a look at the code!
The code base is microscopic compared to Pydantic.UsageSay you have a JSON representing a user in your app looking something like this{"id":530716139,"username":"johndoe","language_code":"en"}You define an envelope for itfromdataclassesimportdataclassfromnvelopeimport(Obj,int_conv,string_conv)@dataclass# note the @dataclass decorator, it is importantclassUser(Obj):_conversion={"id":int_conv,"language_code":string_conv,"username":string_conv,}id:intlanguage_code:strusername:strNow you have a model that knows how to read data from the JSON
(not the raw string, actually, but to the types that are allowed by the
standardjson.dumpsfunction e.g.dict,list,str,int,float,bool,None) ...user=User.from_json({"id":530716139,"username":"johndoe","language_code":"en"})... and knows how to convert itself into JSONUser(id=530716139,username="johndoe",language_code="en",).as_json()# returns a dictionary {#     "id": 530716139,#     "username": "johndoe",#     "language_code": "en"# }Compound envelopesYou can also define compound envelopes.Say we want to define a message and include info about the sender.
Having defined theUserenvelope, we can do it like this:fromnvelopeimportCompoundConv@dataclassclassMessage(Obj):_conversion={"message_id":int_conv,"from_":CompoundConv(User),"text":string_conv,}from_:Usertext:strmessage_id:intand use it the same way:# reading an obj from parsed json like thisMessage.from_json({"message_id":44,"text":"hello there","from_":{"id":530716139,"username":"johndoe","language_code":"en"}})# and dumping an object to json like thisimportjsonjson.dumps(Message(message_id=44,text="whatever",from_=User(id=530716139,username="johndoe",language_code="en",)).as_json())ArraysThis is how you define arrays:fromnvelopeimportArr,CompoundConvclassUsers(Arr):conversion=CompoundConv(User)# Same API inherited from nvelope.Compound interfaceUsers.from_json([{"id":530716139,"username":"johndoe","language_code":"en",},{"id":452341341,"username":"ivandrago","language_code":"ru",}])Users([User(id=530716139,username="johndoe",language_code="en",),User(id=452341341,username="ivandrago",language_code="ru",),]).as_json()Field aliasesAt some point you may need to define an envelope for an API containing certain field names which cannot be
used in python since they are reserved keywords (such asdef,from, etc.).There's a solution for this:fromdataclassesimportdataclassfromnvelopeimportObj,string_conv,CompoundConv,AliasTable@dataclassclassComment(Obj):_conversion={"text":string_conv,"from_":CompoundConv(User),}_alias_table=AliasTable({"from_":"from"})text:strfrom_:UserIn this casefromkey gets replaced byfrom_in the python model.
Thefrom_field gets translated back tofromwhen calling.as_json()Missing and optional fieldsThere's a difference between fields that can be set toNoneand fields which may be missing in the JSON at all.This is how you specify that a some field may be missing from the JSON and that's OK:fromdataclassesimportdataclassfromtypingimportOptionalfromnvelopeimportMaybeMissing,Obj,OptionalConv,AliasTable@dataclassclassComment(Obj):_alias_table=AliasTable({"from_":"from"})text:strimg:Optional[str]# this field can be set to None (null), but is must always be present in the JSONfrom_:MaybeMissing[User]# this field can be missing from JSON body_conversion={"text":string_conv,"img":OptionalConv(string_conv),# note the wrapping with OptionalConv"from_":CompoundConv(User),}This is how you check if theMaybeMissingfield is actually missingcomment.from_.has()# returns False if the field is missingand this is how you get the value:comment.value()# raises an error if there's no value,# so it is recommended to check the output of .has()#  before calling .value()Json-schema supportTheCommentmodel from we have defined generates schema like this:Comment.schema()with the returned schema looking like this:{"type":"object","properties":{"from":{"properties":{"id":{"type":"integer"},"language_code":{"type":"string"},"username":{"type":"string"},},"required":["id","language_code","username"],"type":"object",},"img":{"type":["string","null"]},"text":{"type":"string"},},"required":["text","img"],}NOTE:nvelopedoes not perform json schema checks.Custom conversionsYou may define a custom conversions inheriting fromnvelope.nvelope.Conversionabstract base class
or usingnvelope.nvelope.ConversionOfclass.For example, this is howdatetime_iso_format_convis defined:fromnvelopeimportWithTypeCheckOnDump,ConversionOfdatetime_iso_format_conv=WithTypeCheckOnDump(datetime.datetime,ConversionOf(to_json=lambdav:v.isoformat(),from_json=lambdas:datetime.datetime.fromisoformat(s),),)Say we want to jsonify adatetimefield as POSIX timestamp, instead of storing it in ISO string format.datetime_timestamp_conv=ConversionOf(to_json=lambdav:v.timestamp(),from_json=lambdas:datetime.datetime.fromtimestamp(s),schema={"type":"number"},)We could also addWithTypeCheckOnDumpwrapper in order to add explicit check that
the value passed into.from_json()is indeedfloat.fromnvelopeimportConversionOfdatetime_timestamp_conv=WithTypeCheckOnDump(float,ConversionOf(to_json=lambdav:v.timestamp(),from_json=lambdas:datetime.datetime.fromtimestamp(s),schema={"type":"number"},))You may also go further and implement custom conversion.
Inherit fromnvelope.Conversioninterface, implement its abstract methods, and you are good to go.Custom compoundsYou can also define custom alternatives tonvelope.Objandnvelope.Arr.
It will work fine as long as they inheritnvelope.Compoundinterface.It currently required 3 methods:from_jsonas_jsonschema