Package: tissuebox
TissueboxTissuebox is a pure Pythonic schema validator which takes advantage of
Python’s functional style programming to provide simple yet powerful
validation framework. The standard usage would be validating incoming
JSON objects upon http requests or to validate any Python dict in other
common scenarios.Installation:Usepipto install Tissueboxpip install tissueboxRequirements:Tissuebox requires Python 3.7 however we are considering to add support
for earlier versions of Python3Examples:Assume the incoming JSON object or a python dict which contains hotel
details and we will build upon this example.payload={"name":"Park Shereton","available":True,"price_per_night":270,"email":"contact@shereton.com","web":"www.shereton.com",}1. Validating basic data typesYou can usetissueboxto define a schema to validate the payload
against basic data types and validate usingvalidatemethod.fromtissueboximportvalidatefromtissuebox.basicimportboolean,integer,stringschema={'name':string,'available':boolean,'price_per_night':integer}validate(schema,payload)will return(True,[])2. Validating common datatypesAtissueboxschema is simply a dict where keys are payload keys and
values are type_functions to which the payload value would be passed. A
type_function simply accepts a single parameter and returns a tuple with
two items(boolean, msg).Tissuebox aims to amass a collection of commonly used types to it’s
library. For now common data types likeemail,url,rfc_datetime,geolocationare part oftissuebox’s standard
collections. You can contribute more via Github.fromtissueboximportvalidatefromtissuebox.basicimportemail,integer,string,urlschema={'name':string,'price_per_night':integer,"email":email,"web":url}validate(schema,payload)will return(True,[])One of the waystissueboxstands our from other alternatives is, the
type_functions are stored and passed around as Python variables which is
helpful in identifying the schema definition errors ahead of time as
most IDEs will display squiggly lines if the variables aren’t resolved,
while other frameworks like JsonSchema and Cerebrus pass types within
strings which is hard for IDEs to detect errors in the schema.3. Validating nested fieldsMethod 1:Defining a schema in a nested fashion is very straight forward which
enables re-use schemas around. Consider if the payload has anaddressfield. We can define a separate schema asaddress_schemaand pass it to the main schema as below.fromtissueboximportvalidatefromtissuebox.basicimportemail,integer,string,urlpayload={"name":"Park Shereton","available":True,"price_per_night":270,"email":"contact@shereton.com","web":"www.shereton.com","address":{"street":"128 George St","city":"Sydney","state":"NSW","zip":2000}}address={"street":string,"city":string,"state":string,"zip":integer}schema={'name':string,'price_per_night':integer,"email":email,"web":url,"address":address}validate(schema,payload)would return(True,[])Method 2:The prefered method of defining nested schema is by using.dot as
delimiter to represent nested fields of the payload hierarchy.
Apparently this comes up with the downside wherein if.dot itself
is part of keys which would be an unfortunate scenario. But it can
improve the readability to a tremendous level. See it yourself how
elegantly we can express the schema once we introduce theaddressfield to our payload.schema={'name':string,'price_per_night':integer,"email":email,"web":url,"address.street":string,"address.city":string,"address.state":string,"address.zip":integer}The primary reason why we suggest the later method is we can quickly
define a nested field with any depth without creating unnecessary schema
objects in the middle.4. Validating enums.Let us try enforcing that the fieldaddress.statemust be one of 8
Australian states. Tissuebox let’s you define an enum using the{}i.eset()syntax. Look at the example below.schema={'name':string,'price_per_night':integer,"email":email,"web":url,"address.state":{'ACT','NSW','NT','QLD','SA','TAS','VIC','WA'},"address.zip":integer}To have a feel how Tissuebox responds when we pass something which is
not an Australian statepayload={"name":"Park Shereton","available":True,"price_per_night":270,"email":"contact@shereton.com","web":"www.shereton.com","address":{"street":"128 George St","city":"Sydney","state":"TX","zip":2000}}validate(schema,hotel)would return(False,['["address"]["state"] is failing to be enum of `{\'SA\',\'QLD\',\'NT\',\'TAS\',\'VIC\',\'WA\',\'ACT\',\'NSW\'}`'])5. Validating arraysLet us assume the payload hasstaffswhich is array of staff names.payload={"name":"Park Shereton","email":"contact@shereton.com","web":"www.shereton.com","staffs"["John Doe","Jane Smith"],}Now the schema simple looks as belowschema={'name':string,"email":email,"web":url,"staffs":[string]}So in order to declare an element as array simply use[]syntax, if
it’s array of string simply say[string]. If it’s array of cats
simply say[cat]. Array syntax can be either empty or single length
where the element means a type_function or another nested schema.There are two scenarios where Tissuebox implicitly handles the array.The incoming payload is simply list of dicts then Tissuebox knows
that the given schema must be validated against all the items in the
array.While declaring.dot separated nested attribute, and any of the
middle element is array, Tissuebox is aware of such fact and will
iterate the validation automatically.These two cases are implemented to make Tissuebox as intuitive as
possible,6. Writing custom validatorsBy now you would have observed thattissueboxschema is simply a
collection ofkey:valuepairs wherevaluecontains the data type
verified against.tissueboxdefines them in the style oftype_functionwhich is simply a boolean function that takes one or
more parameters.Let us assume you want to validate the zip code as a valid Australian
one. Sincetissueboxdoes’t have a built-in type function, for that
purpose you can come up with your own type function as below. For
brevity I’ve removed few fields in the payload & schema.>>>defaustralian_zip(x):...# https://www.etl-tools.com/regular-expressions/is-australian-post-code.html...x=str(x)...importre...returnre.match(r'^(0[289][0-9]{2})|([1345689][0-9]{3})|(2[0-8][0-9]{2})|(290[0-9])|(291[0-4])|(7[0-4][0-9]{2})|(7[8-9][0-9]{2})$',x),"must be a valida Australian zip"...>>>hotel={..."address":{..."zip":200...}...}>>>>>>schema={..."address.zip":australian_zip...}>>>>>>validate(schema,hotel)(False,['["address"]["zip"] must be a valida Australian zip"])7. Validating with type_functions that accept parameters.Intissueboxtype_functions always accept one argument which is the
payload value. There are times for a type_function it makes sense to
accepts multiple parameters. To achieve that they are declared as
Python’s higher order functions.Let us try validating where theprice_per_nightmust be multiple of
50. Also let us declare the Yelp review rating of a hotel must be
between 1-5.>>>fromtissueboximportvalidate>>>fromtissuebox.basicimportbetween,divisible,string>>>schema={..."name":string,..."rating":between(1,5),..."price_per_night":divisible(50)...}>>>>>>hotel={..."name":"Park Shereton",..."price_per_night":370,..."rating":5.1...}>>>>>>validate(schema,hotel)(False,['["price_per_night"] is failing to be `divisible(50)`','["rating"] is failing to be `between(1, 5)`'])For curiosity here is the implementation ofdivisiblefrom Tissuebox
library. It has been defined as a higher order function which returns
another function which always accepts single parameter. While writing
custom validators you are encouraged to use the same pattern.defdivisible(n):defdivisible(x):returnnumeric(x)andnumeric(n)andx%n==0,"multiple of{}".format(n)returndivisible8. Combining multiple type_functions for same elementAs we have observedtissueboxschema is a dict withkey:valueformat. In Python keys in dicts are unique. It’s a terrible idea to
redeclare same key since the data will be overridden.Assume that you are attempting to do something like thisfromtissuebox.basicimportdivisible,integer,positive,stringschema={'name':string,'price_per_night':integer,'price_per_night':positive,'price_per_night':divisible(50),"address.zip":integer}Hereprice_per_nightwill be overridden by the latest declaration
which must be avoided. This can be solved with another special syntax
which yet PythonicSimply use()to chain type_functions.```python
from tissuebox.basic import divisible, integer, positive, string

schema = {
    'name': string,
    'price_per_night': (integer, positive, divisible(50)),
    "address.zip": integer
}
```Now Tissuebox will iterate all these conditions againstprice_per_night9. Declaring a field asrequiredWhile Tissuebox validates the values with type_functions, it only does
so only for the values are found in the payload. Otherwise they were
simply ignored silently.In a situation where a specific value is expected in payload declared
them asrequiredfunction. And it’s a common scenario to combine
them under()operator as described in the above.fromtissuebox.basicimportinteger,required,stringschema={'name':(required,string),"address.city":(required,string),"address.zip":integer}Tissuebox Advantages:Tissuebox has lots of advantages than the current alternatives like
jsonschema, cerebrus etc.Truly Pythonic and heavily relies on short & static methods. The
schema definition itself takes full advantages of Python’s built-in
syntax like{}for enum,()for parameterized function,[]chaining multiple rules etcHighly readable with concise schema definition.Highly extensible with ability to insert your own custom methods
without complicated class inheritance.Ability to provide all the error messages upfront upon validation.