Derived Validators¶
TypedDictValidator, DataclassValidator, and NamedTupleValidator
all accept dicts for validation, and largely share the same API. Here’s a quick
example of using TypedDictValidator:
from typing import Optional, TypedDict
from koda_validate import TypedDictValidator, Valid
class Image(TypedDict):
height: int
width: int
description: Optional[str]
validator = TypedDictValidator(Image)
assert validator({"height": 10, "width": 20, "description": None}) == Valid(
{"height": 10, "width": 20, "description": None}
)
Here’s an equivalent example with DataclassValidator
from typing import Optional
from dataclasses import dataclass
from koda_validate import DataclassValidator, Valid
@dataclass
class Image:
height: int
width: int
description: Optional[str]
validator = DataclassValidator(Image)
assert validator({"height": 10, "width": 20, "description": None}) == Valid(
Image(height=10, width=20, description=None)
)
Note
A similar example for NamedTupleValidator would be nearly identical to the
DataclassValidator example directly above. As such, it’s left as an exercise
for the reader.
Supported Typehints¶
The range of typehints that are automatically converted to Validators is fairly
extensive, and you can build complex nested validators, even using things like
Literal and Union types:
from dataclasses import dataclass
from typing import Literal, Optional, TypedDict, Union
from koda_validate import TypedDictValidator, Valid
@dataclass
class Ingredient:
quantity: Union[int, float]
unit: Optional[Literal["teaspoon", "tablespoon"]] # etc...
name: str
class Recipe(TypedDict):
title: str
ingredients: list[Ingredient]
instructions: str
recipe_validator = TypedDictValidator(Recipe)
result = recipe_validator(
{
"title": "Peanut Butter and Jelly Sandwich",
"ingredients": [
{"quantity": 2, "unit": None, "name": "slices of bread"},
{"quantity": 2, "unit": "tablespoon", "name": "peanut butter"},
{"quantity": 4.5, "unit": "teaspoon", "name": "jelly"},
],
"instructions": "spread the peanut butter and jelly onto the bread",
}
)
assert isinstance(result, Valid)
assert result.val["title"] == "Peanut Butter and Jelly Sandwich"
If a typehint is not supported, an exception will be thrown. You can handle unhandled typehints a with custom typehint_resolver function.
Optional Keys¶
Each of these validators allows for the specification of optional keys, but the three
Validators don’t all share the same API. For DataclassValidator and
NamedTupleValidator keys are understood to be optional if a default value is defined
for a given attribute.
from typing import NamedTuple
from koda_validate import NamedTupleValidator, Valid
class SomeType(NamedTuple):
a: str
b: int = 10
validator = NamedTupleValidator(SomeType)
assert validator({"a": "ok"}) == Valid(SomeType("ok", 10))
For TypedDictValidator, Koda Validate simply abides by the contents of the
__optional_keys__ attribute. Take a look at the TypedDict docs for
information on how to specific optional keys on TypedDicts.
Extra Keys¶
TypedDictValidator, DataclassValidator, and NamedTupleValidator
can all be configured to fail if extra keys are found – simply pass
fail_on_unknown_keys=True at initialization.
from dataclasses import dataclass
from koda_validate import DataclassValidator, Valid, Invalid
@dataclass
class Example:
a: str
b: float
test_dict = {"a": "ok", "b": 2.0, "c": None}
validator_no_unknown_keys = DataclassValidator(Example, fail_on_unknown_keys=True)
assert isinstance(validator_no_unknown_keys(test_dict), Invalid)
validator_unknown_keys_ok = DataclassValidator(Example)
assert isinstance(validator_unknown_keys_ok(test_dict), Valid)
Customization¶
It’s common to need custom logic for derived Validators. There are several ways
to achieve that.
Annotated¶
In Python 3.9+, you can use Annotated
to add a custom Validator for a given key.
from dataclasses import dataclass
from typing import Annotated, Optional
from koda_validate import (IntValidator, DataclassValidator, PredicateErrs,
Min, Max, Valid, Invalid, KeyErrs)
@dataclass
class Image:
height: Annotated[int, IntValidator(Min(10), Max(1000))]
width: Annotated[int, IntValidator(Min(10), Max(1000))]
description: Optional[str] = None
validator = DataclassValidator(Image)
assert validator({"height": 50, "width": 100, "description": "wow"}) == Valid(
Image(50, 100, "wow")
)
assert validator({"height": 1, "width": 100, "description": "wow"}) == Invalid(
KeyErrs({
'height': Invalid(PredicateErrs([Min(10)]),
1,
IntValidator(Min(10), Max(1000)))}
),
{'height': 1, 'width': 100, 'description': 'wow'},
validator
)
Overrides¶
If you don’t want to add Annotated to your class annotations,
you can use overrides={<key>: <validator>}. The following will produce the same
Validator as in the Annotated example above.
from dataclasses import dataclass
from typing import Annotated, Optional
from koda_validate import DataclassValidator, IntValidator, Min, Max
@dataclass
class Image:
height: int
width: int
description: Optional[str] = None
validator = DataclassValidator(Image, overrides={
"height": IntValidator(Min(10), Max(1000)),
"width": IntValidator(Min(10), Max(1000))
})
typehint_resolver¶
The typehint_resolver parameter controls how Derived Validators
resolve typehints into Validators. This example will produce the same Validator as in
the Annotated example.
from dataclasses import dataclass
from typing import Any, Optional
from koda_validate import Validator, IntValidator, Min, Max, DataclassValidator
from koda_validate.typehints import get_typehint_validator
@dataclass
class Image:
height: int
width: int
description: Optional[str] = None
def custom_resolver(annotations: Any) -> Validator[Any]:
# we're only customizing how `int`s are handled
if annotations is int:
return IntValidator(Min(10), Max(1000))
else:
return get_typehint_validator(annotations)
validator = DataclassValidator(Image, typehint_resolver=custom_resolver)
It often makes sense to wrap get_typehint_validator, as in the example
above, but it’s OK to completely rewrite how this works if it suits you.
validate_object and validate_object_async¶
You can pass either validate_object or validate_object_async to Derived Validators,
which will run after the individual attributes have been validated:
from dataclasses import dataclass
from typing import Optional
from koda_validate import (DataclassValidator, Invalid, Valid,
ValidationErrBase, ErrType)
@dataclass
class QA:
question_id: int
answer: str
@dataclass
class WrongAnswerErr(ValidationErrBase):
pass
def answer_is_valid(obj: QA) -> Optional[ErrType]:
# really sophisticated logic here!
if obj.question_id == 100 and obj.answer == "the right answer":
# success
return None
else:
return WrongAnswerErr()
validator = DataclassValidator(QA, validate_object=answer_is_valid)
assert validator(
{"question_id": 100, "answer": "wrong answer :("}
) == Invalid(WrongAnswerErr(), QA(100, 'wrong answer :('), validator)
assert validator(
{"question_id": 100, "answer": "the right answer"}
) == Valid(QA(100, 'the right answer'))
Caveats¶
Some notable limitations exist with derived dictionary Validators:
the keys of the dictionaries must be strings
the keys must abide by the relevant attribute name restrictions for the classes
generic and custom types will often require a custom
Validator