A zero dependency Python library for defining schemas, parsing and validating payloads.
Liaison doesn't aim to be too clever. It doesn't use descriptors, fancy metaprogramming or type hints for defining
your schema. Simply inherit from the Schema
base class, define your fields and call parse
. In return, you'll
receive a simple Namespace
object containing your parsed data.
Goals:
- Simplicity
- Extensibility
- Speed
- 100% test coverage
Installation:
pip install liaison
Example:
from liaison import Schema, ValidationError
from liaison.fields import StringField, IntField, BoolField, ListField, DateTimeField
class UserSchema(Schema):
name = StringField(required=True)
email = StringField(required=True)
age = IntField(min_val=18)
date_of_birth = DateTimeField(date_format="%d-%m-%Y")
subscribed = BoolField(default=False)
tags = ListField(min_len=1)
data = {
"name": "Bar",
"email": "foo@bar.com",
"age": 21,
"tags": ["Python"]
}
result = UserSchema.parse(data)
print(result.name, result.email, result.age, result.tags) # Bar foo@bar.com 21 ['Python']
Handling validation errors:
data = {
"name": "Bar",
"email": "foo@bar.com",
"age": 16
}
try:
result = UserSchema.parse(data)
except ValidationError as e:
print(e) # Value for 'age' must be at least 18
Defining custom field validators via the <field>.validator
decorator:
class UserSchema(Schema):
name = StringField(required=True)
email = StringField(required=True)
age = IntField(min_val=18)
@name.validator
def validate_name(self, key, value):
# Define a custom validator, overrides the default validation method
if value == "Foo":
raise ValidationError(f"'{value}' is not a valid value for '{key}'")
return value
Custom validators can also be passed as a parameter to the field:
def name_validator(schema_cls, key, value):
if value in ("Foo", "Bar", "Baz"):
raise ValidationError(f"'{value}' is not a valid value for '{key}'")
return value
class UserSchema(Schema):
name = StringField(required=True, validator=name_validator)
email = StringField(required=True)
age = IntField(min_val=18)
Use fields to define your schema. By default, all fields accept the following common parameters:
Parameter | Type | Description | Default |
---|---|---|---|
required |
bool |
If the value is required | False |
default |
Any |
A default value | None |
choices |
List[Any] |
A list of choices | None |
validator |
Callable |
A function to override the default validation method | None |
strict_type |
bool |
If True , only accept the fields data type |
False |
Parameter | Type | Description | Default |
---|---|---|---|
min_len |
int |
The minimum length | None |
max_len |
int |
The maximum length | None |
Parameter | Type | Description | Default |
---|---|---|---|
min_val |
int |
The minimum value | None |
max_val |
int |
The maximum value | None |
Parameter | Type | Description | Default |
---|---|---|---|
min_val |
int |
The minimum value | None |
max_val |
int |
The maximum value | None |
Parameter | Type | Description | Default |
---|---|---|---|
min_len |
int |
The minimum length | None |
max_len |
int |
The maximum length | None |
Note -
SetField
shares the same behaviour asListField
, returning aset
.
Parameter | Type | Description | Default |
---|---|---|---|
min_len |
int |
The minimum length | None |
max_len |
int |
The maximum length | None |
Parameter | Type | Description | Default |
---|---|---|---|
min_len |
int |
The minimum length | None |
max_len |
int |
The maximum length | None |
Note -
DateTimeField
fields will returndatetime
objects
Parameter | Type | Description |
---|---|---|
date_format |
str |
The date format |
Note -
UUIDField
fields will NOT return aUUID
obejct, it will return a string.
Calling the parse
method on a Schema
object will return a Namespace
object, holding the parsed values as
attributes.
from liaison import Schema
from liaison.fields import StringField, IntField, BoolField, FloatField, UUIDField
class RESTBaseSchema(Schema):
offset = IntField(min_val=0, default=0)
limit = IntField(max_val=100)
search = StringField()
class ProductsRESTSchema(RESTBaseSchema):
product_id = UUIDField()
category = StringField()
price = FloatField()
in_stock = BoolField()
payload = {
"offset": 10,
"category": "shoes",
"in_stock": True
}
result = ProductsRESTSchema.parse(payload)
print(result.offset, result.limit, result.search, result.category, result.in_stock)
# 10 None None shoes True
Namespace
objects have a to_dict
method, returning a dictionary of the Namespace
attributes and values:
print(result.to_dict())
# {'category': 'shoes', 'in_stock': True, 'limit': None, 'offset': 10, 'price': None, 'product_id': None, 'search': None}
An optional exclude
parameter can be included to exclude certain attributes:
print(result.to_dict(exclude=("offset", "limit", "search")))
# {'category': 'shoes', 'in_stock': True, 'price': None, 'product_id': None}
Create your own fields and validation logic by inheriting from any of the field classes and implementing a
validate
method.
Note - The
validate
method must accept 2 params (key, value)
from liaison import Schema, ValidationError
from liaison.fields import StringField
class PasswordField(StringField):
def validate(self, key, value):
value = super().validate(key, value)
if len(value) < 9:
raise ValidationError("Value for 'password' must be at least 9 characters in length")
# etc...
return value
class UserSchema(Schema):
username = StringField(required=True)
password = PasswordField()
payload = {
"username": "FooBar",
"password": "password"
}
try:
result = UserSchema.parse(payload)
except ValidationError as e:
print(e) # Value for 'password' must be at least 9 characters in length
payload = {
"username": "FooBar",
"password": "password12345!"
}
result = UserSchema.parse(payload)
print(result.password) # password12345!