Differences from Cerberus
Transformation AND validation
Sureberus exists because Cerberus wasn't flexible enough for our use. Most importantly, Cerberus strictly separates transformation (what the Cerberus documentation calls "Normalization") from validation; if you want to transform a document with Cerberus, you can't also make sure it's valid at the same time. This can lead to some surprising limitations.
For example,
from sureberus import normalize_dict
from cerberus import Validator
schema = {
"x": {
"anyof": [
{"type": "dict", "schema": {"y": {"type": "integer", "default": 0}}},
{"type": "integer"},
]
}
}
Here we have a schema that says:
- this is a dict
- whose
x
field can either be- an integer,
- or a dict,
- containing a
y
field which defaults to 0.
- containing a
- whose
Let's try using it with Sureberus.
assert normalize_dict(schema, {"x": {}}) == {"x": {"y": 0}}
assert normalize_dict(schema, {"x": 5}) == {"x": 5}
These assertions run fine. Sureberus tries to normalize the value with each schema in turn, and returns the result of the first one that succeeds.
Now let's try with Cerberus.
v = Validator(schema)
assert v.normalized({"x": {}}) == {"x": {"y": 0}} # This fails!
assert v.normalized({"x": 5}) == {"x": 5}
The first assertion fails, since Cerberus is returning {'x': {}}
-- it seems to be completely disregarding our default
directive. Why is this?
It's actually deeper than that, still. Let's see what happens when we pass something that obviously shouldn't even validate:
# Sureberus:
from sureberus.errors import NoneMatched
with pytest.raises(NoneMatched):
normalize_dict(schema, {"x": "foo"})
# Cerberus:
with pytest.raises(Exception): # This fails!
v.normalized({"x": "foo"})
Cerberus returns the original document without throwing any sort of exception, even though our schema indicates that the x
key must have a value that's either an integer or a dict.
This is expected as per Cerberus's documentation: you have to validate separately from normalization, by using either the validate
method or the normalized
method.
But because it separates these concepts so strictly, and because some directives like anyof
are considered only validation rules and not normalization rules,
it's impossible to express the transformation we want.
Schema Selection
To improve upon the poor error messages that can occur when using "variable schemas" (the oneof
and anyof
directives) in Cerberus,
we've implemented facilities in Sureberus that make it much more clear how to choose schemas, with the choose_schema
directive.
Not only does this make the schema easier to reason about, it makes error messages much nicer: with anyof
, we have to say:
"Sorry, your value didn't match this schema, or that schema, or that schema..."
But with the mechanisms available through choose_schema
, we get to say:
"I know you want to use THIS schema, because you had a field in your dictionary that indicated which schema to use. This is how it doesn't match..."
The choose_schema
facility is documented more thoroughly in Schema selection.
In-line schema registries
In Cerberus, you have to invoke Python code to register schemas.
This means you can't describe a recursive schema without writing custom Python code (as far as I have been able to figure out, anyway).
With Sureberus, you can take advantage of the registry
directive which allows you to declare named schemas.
This means that recursive schemas are easy to define in Sureberus.
See Schema registries for more information.