zon

zon - Zod-like validator library for Python

Coverage Status

Want to have the validation power of zod but with the ease-of-use of Python?

Enter zon.

zon is a Python library that aims to provide a simple, easy-to-use API for validating data, similar to zod’s own API’. In fact, the whole library and its name were inspired by zod: Zod + Python = Zon !!!.

Why

While doing a project for college, we were using both Python and JS/TS in the code-base. We were also using a JSON-Schema document to define a data schema for the data that we would be consuming.

There exists tooling that allows for one schema to generate TS types and from that Zod validator code, but there was nothing of the sort for Python. What’s more, none of the validation libraries I found for Python had an API that was as easy to use as Zod’s. So, I set out to build it.

Installation

Pip

This package is available on PyPI, so you can install it with pip:

pip install zon

Source

Alternatively, you can clone this repository and install it from source:

git clone https://github.com/Naapperas/zon
cd zon
pip install .

Usage

In its essence, zon behaves much like zod. If you have used zod, you will feel right at home with zon.

[!NOTE]
There are some differences in the public API between zon and zod. Those mostly stem from the fact that Python does not have type inference like Typescript has. There are other slight deviations between zon and zod.

Basic types

zon features most of zod’s basic types:

zon.string()
zon.number()
zon.boolean()
zon.literal()
zon.enum()

zon.record()
zon.element_list()
zon.element_tuple()

zon.union()
zon.intersection()
zon.optional()

zon.never()
zon.anything()

Numbers

validator = zon.number()

validator.gt(5)
validator.gte(5) # alias for .min(5)
validator.lt(5)
validator.lte(5) # alias for .max(5)

validator.int()
validator.float()

validator.positive()
validator.negative()
validator.non_negative()
validator.non_positive()

validator.multiple_of(5) # alias for .step(5)

validator.finite()

Strings

For strings, there are also some extra methods:

validator = zon.string()

validator.min(5)
validator.max(10)
validator.length(5)

validator.email()
validator.url()
validator.uuid()
validator.regex(r"^\d{3}-\d{3}-\d{4}$")
validator.includes("needle")
validator.starts_with("prefix")
validator.ends_with("suffix")
validator.datetime()
validator.ip()

# transformers
validator.trim()
validator.to_lower_case()
validator.to_upper_case()

Datetime

zon accepts the same options as zod for datetime string validation:

validator.datetime({"precision": 3})
validator.datetime({"local": True})
validator.datetime({"offset": True})

[!NOTE] zod uses regex-based validation for datetimes, which must be valid ISO 8601 strings. However, due to an issue with most JavaScript engines’ datetime validation, offsets cannot specify only hours, and zod reflects this in their API. While zon could reflect zod’s API in this matter, it is best to not constrain users to the problems of another platform, making this one of the aspects where zon deviates from zod.

IP addresses

zon accepts the same options as zod for ip address string validation:

validator.ip({"version": "v4"})
validator.ip({"version": "v6"})

List

Lists are defined by calling the zon.element_list() method, passing as an argument a Zon instance. All elements in this list must be of the same type.

zon.element_list(zon.string())

Like strings, lists also have some extra methods that check the length of the list:

validator = zon.list(...)

validator.min(5)
validator.max(10)
validator.length(5)

There is also a method for validating that the list has at least one element, nonempty:

validator.nonempty()

You can get the type of the list’s elements by accessing the element property:

zon.element_list(zon.string()).element is ZonString

Union

zon supports unions of types, which are defined by calling the zon.union() method, passing as arguments the Zon instances that are part of the union.

zod.union([zon.string(), zon.number()])

To access the various options, access the options property:

zod.union([zon.string(), zon.number()]).options = [ZonString, ZonNumber]

Record

zon supports validating objects according to a specified schema, using the zon.schema() method. This method takes as an argument a dictionary, where the keys are the keys of the object to be validated and the values are the Zon instances that define the type of each key.

validator = zon.record({
    "name": zon.string(),
    "age": zon.number(),
    "isAwesome": zon.boolean(),
    "friends": zon.array(zon.string()),
    "address": zon.record({
        "street": zon.string(),
        "city": zon.string(),
        "country": zon.string(),
    }),
})

Useful methods for ZonRecord instances:

validator.extend({...})
validator.merge(otherZon) == validator.extend(otherZon.shape)
validator.pick({"name": True}) == ZonRecord({"name": ...})
validator.omit({"name": True}) == ZonRecord({"<key that is different from name>": ..., ...})
validator.partial() # makes all attributes optional, shallowly
validator.partial({"name": True}) # makes only "name" optional (in this example)
validator.deepPartial() # makes all attributes optional, recursively
validator.required() # makes all attributes required, shallowly

You can access the shape of objects validated by any ZonRecord instance by accessing the shape property:

shape = validator.shape

If you want to validate only the keys of the shape, use the keyof method:

validator.keyof() == ZonEnum(["name", "age", "isAwesome", "friends", "address"])

Unknown Keys

As zod, zon normally strips unknown keys from records. This, however, can be configured:

validator.strict() # presence of unknown keys makes validation fail
validator.passthrough() # add unknown keys to the resulting record
validator.strip() # the default behavior, strip unknown keys from the resulting record

Catchall

In case you want to validate unknown keys, you can use the catchall method to specify the validator that is used:

validator.catchall(zon.string()) # unknown keys *must* be associated with string values

Tuple

zon supports tuples out-of-the-box, which are fixed-size containers whose elements might not have the same type:

validator = zon.tuple([...])

If you want to access the items of the tuples, use the items property:

validator.items = [...]

Variadic items can be validated using the rest method:

validator.rest(zon.string()) # after the defined items, everything must be a string

Enums

zon supports enumerations, which allow validating that any given data is one of many values:

validator = zon.enum([...])

If you want to access the possible valid values, use the enum property:

validator.enum = [...]

Examples

Example usage of zon can be found in the examples directory.

Documentation

Documentation is still not available, but it will be soon.

Tests

Tests can be found in the tests folder. zon uses pytest for unit testing.

To run the tests, simply run:

pytest test

Coverage can be found on Coveralls.

Contributing

Contribution guidelines can be found in CONTRIBUTING

Past and current contributors can be found in CONTRIBUTORS

License

This project is licensed under the MIT License - see the LICENSE file for details.