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 List, 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’re using Python3.8, or 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