Error handling

Validobj aims to be good at providing good error messages by default, and enough metadata so that errors can be repurposed or improved. That metadata is enough to for example attach YAML line numbers to the sources of error.

Generic errors

All exceptions raised by Validobj are subclasses of validobj.errors.ValidationError. Therefore it can be used in an except block to tell if the validation succeeded.

>>> try:
...     result = validobj.parse_input([1, 2, "3"], list)
... except validobj.ValidationError as e:
...     print("Validation failed")
... else:
...     print("Validation succeeded")
...
Validation succeeded

Errors for Unions

Errors for union types (including typing.Optional) result in a validobj.errors.UnionValidationError being raised. The exception contains a causes attribute containing the reasons for the mismatch for each of the individual types.

>>> from typing import Optional, List
>>> validobj.parse_input([1, "dos"], Optional[List[int]])
Traceback (most recent call last):
...
UnionValidationError: No match for any possible type:
Not a valid match for 'typing.List[int]': Cannot process list item 2.
Not a valid match for 'NoneType': Expecting value of type 'NoneType', not list.

Key mismatch

The optional and required keys in a mapping can be specified through the definition of a dataclass. For example:

import dataclasses
from typing import Optional, List


@dataclasses.dataclass
class User:
    name: str
    phone: Optional[str] = None
    tasks: List[str] = dataclasses.field(default_factory=list)

means that the class User has one required field, name (because it doesn’t have a default) and two optional fields, phone and tasks (because they have a simple value default and a default factory respectively).

When provided values do not match the specification, either because unknown keys are provided or required keys are missing, a validobj.errors.WrongKeysError is raised. The error knows about unknown, missing and valid keys and stores them in as the unkown, missing and valid attributes respectively. Suggestions will be given for invalid keys that look like typos.

>>> validobj.parse_input({
...      'phone': '555-1337-000', 'address': 'Somewhereville', 'nme': 'Zahari'}, User
... )
Traceback (most recent call last):
...
WrongKeysError: Cannot process value into 'User' because fields do not match.
The following required keys are missing: {'name'}. The following keys are unknown: {'nme', 'address'}.
Alternatives to invalid value 'nme' include:
  - name

All valid options are:
  - name
  - phone
  - tasks

The attributes of the exception can be inspected:

>>> from validobj.errors import WrongKeysError
>>> try:
...     validobj.parse_input({'phone': '555-1337-000',
...         'address': 'Somewhereville', 'nme': 'Zahari'},
...     User)
... except WrongKeysError as e:
...     print(f'The missing keys are  {sorted(e.missing)!r}')
...
The missing keys are  ['name']

Wrong keys

When a given value in a mapping fails to be processed, the original exception is wrapped with a validobj.errors.WrongFieldError so that it is its __cause__. The problematic field is stored in the wrong_field attribute:

>>> validobj.parse_input({'name': 11}, User) 
Traceback (most recent call last):
...
WrongTypeError: Expecting value of type 'str', not int.

The above exception was the direct cause of the following exception:
...
WrongFieldError: Cannot process field 'name' of value into the corresponding field of 'User'

Wrong list items

Analogously to mapping keys, when a given list item fails to conform to the specification, a validobj.errors.WrongListItemError is raised. The problematic index is stored in the wrong_index attribute of the exception. The original error is stored as the __cause__.

>>> validobj.parse_input([{'name': "Eleven"}, {'name': 11}], List[User]) 
Traceback (most recent call last):
    ...
WrongTypeError: Expecting value of type 'str', not int.
    ...
The above exception was the direct cause of the following exception:
    ...
Traceback (most recent call last):
    ...
WrongFieldError: Cannot process field 'name' of value into the corresponding field of 'User'
    ...
The above exception was the direct cause of the following exception:
    ...
Traceback (most recent call last):
     ...
WrongListItemError: Cannot process list item 2.

Note that there are as many levels of chaining as necessary.

Wrong enum elements

Wrong enum elements will result in a validobj.errors.NotAnEnumItemError. These errors know about the original enum class and will suggest fixes to the typos. Additionally enum.Flag combinations will behave like lists and raise a validobj.errors.WrongListItemError.

>>> import enum
>>> import validobj
>>> class DiskPermissions(enum.Flag):
...     READ = enum.auto()
...     WRITE = enum.auto()
...     EXECUTE = enum.auto()
...
>>> validobj.parse_input(['EXECUTE', 'RAED'], DiskPermissions) 
NotAnEnumItemError                        Traceback (most recent call last)
...
NotAnEnumItemError: 'RAED' is not a valid member of 'DiskPermissions'.
Alternatives to invalid value 'RAED' include:
  - READ
All valid values are:
  - READ
  - WRITE
  - EXECUTE

The above exception was the direct cause of the following exception:
...
WrongListItemError: Cannot process item 2 into 'DiskPermissions'.

Wrong Literals

Wrong Literals provide an exception with the tested and valid values:

>>> import validobj
>>> import typing
>>> validobj.parse_input(5, typing.Literal[6, typing.Literal[7], 8])
Traceback (most recent call last):
...
WrongLiteralError: Wrong literal. Expecting one of '[6, 7, 8]'. Got '5'