Skip to main content

Tissuebox :: Pythonic payload validator

Project description

https://raw.githubusercontent.com/n3h3m/tissuebox/master/tissuebox.png

Tissuebox

Tissuebox 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:

Use pip to install Tissuebox

pip install tissuebox

Requirements:

Tissuebox requires Python 3.7 however we are considering to add support for earlier versions of Python3

Examples:

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 types

You can use tissuebox to define a schema to validate the payload against basic data types and validate using validate method.

from tissuebox import validate
from tissuebox.basic import boolean, integer, string

schema = {
    'name': string,
    'available': boolean,
    'price_per_night': integer
}

validate(payload, schema)

will return

(True, [])

2. Validating common datatypes

A tissuebox schema 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 like email, url, rfc_datetime, geolocation are part of tissuebox’s standard collections. You can contribute more via Github.

from tissuebox import validate
from tissuebox.basic import email, integer, string, url
schema = {
    'name': string,
    'price_per_night': integer,
    "email": email,
    "web": url
}

validate(payload, schema)

will return

(True, [])

One of the ways tissuebox stands 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 fields

Method 1:

Defining a schema in a nested fashion is very straight forward which enables re-use schemas around. Consider if the payload has an address field. We can define a separate schema as address_schema and pass it to the main schema as below.

from tissuebox import validate
from tissuebox.basic import email, integer, string, url
payload = {
    "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(payload, schema)

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 the address field 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 field address.state must be one of 8 Australian states. Tissuebox let’s you define an enum using the {} i.e set() 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 state

payload = {
    "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(hotel, schema)

would return

(False, ['["address"]["state"] is failing to be enum of `{\'SA\', \'QLD\', \'NT\', \'TAS\', \'VIC\', \'WA\', \'ACT\', \'NSW\'}`'])

5. Validating arrays

Let us assume the payload has staffs which 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 below

schema = {
    '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.

  1. 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.

  2. 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 validators

By now you would have observed that tissuebox schema is simply a collection of key:value pairs where value contains the data type verified against. tissuebox defines them in the style of type_function which 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. Since tissuebox does’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.

>>> def australian_zip(x):
...     # https://www.etl-tools.com/regular-expressions/is-australian-post-code.html
...     x = str(x)
...     import re
...     return re.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(hotel, schema)
(False, ['["address"]["zip"] must be a valida Australian zip"])

7. Validating with type_functions that accept parameters.

In tissuebox type_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 the price_per_night must be multiple of 50. Also let us declare the Yelp review rating of a hotel must be between 1-5.

>>> from tissuebox import validate
>>> from tissuebox.basic import between, 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(hotel, schema)
(False, [
    '["price_per_night"] is failing to be `divisible(50)`',
    '["rating"] is failing to be `between(1, 5)`'
    ])

For curiosity here is the implementation of divisible from 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.

def divisible(n):
    def divisible(x):
        return numeric(x) and numeric(n) and x % n == 0, "multiple of {}".format(n)

    return divisible

8. Combining multiple type_functions for same element

As we have observed tissuebox schema is a dict with key:value format. 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 this

from tissuebox.basic import divisible, integer, positive, string
schema = {
    'name': string,
    'price_per_night': integer,
    'price_per_night': positive,
    'price_per_night': divisible(50),
    "address.zip": integer
}

Here price_per_night will be overridden by the latest declaration which must be avoided. This can be solved with another special syntax which yet Pythonic

Simply 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 against price_per_night

9. Declaring a field as required

While 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 as required function. And it’s a common scenario to combine them under () operator as described in the above.

from tissuebox.basic import integer, required, string
schema = {
    '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 etc

  • Highly 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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tissuebox-26.2.0.tar.gz (15.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tissuebox-26.2.0-py3-none-any.whl (9.4 kB view details)

Uploaded Python 3

File details

Details for the file tissuebox-26.2.0.tar.gz.

File metadata

  • Download URL: tissuebox-26.2.0.tar.gz
  • Upload date:
  • Size: 15.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for tissuebox-26.2.0.tar.gz
Algorithm Hash digest
SHA256 6bde818f2cbb218cdb2de1639cbc95a58bfa4a469406bf12ae8049d65ac741c7
MD5 5ce667bc9325e271bfcf57f670247303
BLAKE2b-256 d38c9adafa04ee1510babd52eab3a997040e0e4358c05b7c224ff4c0e22d5f97

See more details on using hashes here.

File details

Details for the file tissuebox-26.2.0-py3-none-any.whl.

File metadata

  • Download URL: tissuebox-26.2.0-py3-none-any.whl
  • Upload date:
  • Size: 9.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for tissuebox-26.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 daf7085562f2ee53cd95f282674911f5b6267bdb480d2ccf055f55150f507993
MD5 73b5bd82df8da08de47beda8ce4d6532
BLAKE2b-256 67f1e8b359a5f05e466d849a226d0262ff6ce6508beaabe9e3501040313d32de

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page