Input and output

validobj.validation.parse_input() takes an input to be processed and andoutput specification. The useful values for these options and the transformations that result are described below.

Supported input

Validobj is tested for input that can be processed from JSON. This includes:

  • Integers and floats
  • Strings
  • Booleans
  • None
  • Lists
  • Mappings with string keys (although in practice any hashable key should work)

Other “scalar” types, such as datetimes processed from YAML should work fine. However these have no tests and no effort is made to avoid corner cases for more general inputs.

Supported output

The above is concerted into a wider set of Python objects with additional restrictions on the type, supported by the typing module.

Simple verbatim input

All of the above input is supported verbatim

>>> validobj.parse_input({'a': 4, 'b': [1,2,"tres", None]}, dict)
{'a': 4, 'b': [1, 2, 'tres', None]}

Following typing, type(None) can be simply written as None.

>>> validobj.parse_input(None, None)

Collections

Lists can be automatically converted to tuples, sets or frozensets.

>>> validobj.parse_input([1,2,3],  frozenset)
frozenset({1, 2, 3})

as well as typed version of the above:

>>> import typing
>>> validobj.parse_input([1,2,3],  typing.FrozenSet[int])
frozenset({1, 2, 3})
>>> validobj.parse_input([1,2,'x'],  typing.FrozenSet[int]) 
Traceback (most recent call last):
...
validobj.errors.WrongTypeError: Expecting value of type 'int', not str.

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
...
validobj.errors.WrongListItemError: Cannot process list item 3.

The types of the elements of a tuple can be specified either for each element or made homogeneous:

>>> validobj.parse_input([1,2,'x'],  typing.Tuple[int, int, str])
(1, 2, 'x')
>>> validobj.parse_input([1,2,3],  typing.Tuple[int, ...])
(1, 2, 3)
>>> validobj.parse_input([1,2,'x'],  typing.Tuple[int, int])
Traceback (most recent call last):
...
validobj.errors.ValidationError: Expecting value of length 2, not 3
>>> validobj.parse_input([1,2,3, 'x'],  typing.Tuple[int, ...]) 
Traceback (most recent call last):
...
validobj.errors.WrongTypeError: Expecting value of type 'int', not str.

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
...
validobj.errors.WrongListItemError: Cannot process list item 4.

Unions

typing.Union and typing.Optional are supported:

>>> validobj.parse_input("Hello Zah", typing.Union[str, int] )
'Hello Zah'

>>> validobj.parse_input([None, 6],  typing.Tuple[typing.Optional[str], int])
(None, 6)

If a given input can be coerced into more than one of the member of the union, then the order matters:

>>> validobj.parse_input([1,2,3], typing.Union[tuple, set])
(1, 2, 3)
>>> validobj.parse_input([1,2,3], typing.Union[set, tuple])
{1, 2, 3}

From Python 3.10, union types can be specified using the X | Y syntax.

>>> validobj.parse_input([1,2,3], tuple | set)
(1, 2, 3)

Literals

typing.Literal is supported with recent enough versions of the typing module:

>>> validobj.parse_input(5, typing.Literal[1, 2, typing.Literal[5]])
5

Annotaded

typing.Annotated is used to enable custom processing of types. Other annotation metadata is ignored.

>>> validobj.parse_input(5, typing.Annotated[int, "bogus"])
5

Any

typing.Any is a no-op:

>>> validobj.parse_input('Hello', typing.Any)
'Hello'

Typed mappings

typing.TypedDict is supported for Python versions older than 3.9, including with nesting of types.

>>> class Config(typing.TypedDict):
...     a: str
...     b: typing.Optional[typing.List[int]]
...
>>> validobj.parse_input({"a": "Hello", "b": [1,2,3]}, Config)
{'a': 'Hello', 'b': [1, 2, 3]}
>>> validobj.parse_input({"a": "Hello", "b": [1,2,"three"]}, Config) 
...
WrongFieldError: Cannot process field 'b' of value into the corresponding field of 'Config'

typing.Mapping can be used to restrict types of keys and values, for arbitrary keys;

>>> validobj.parse_input({'key': 'value', 'quantity': 5}, typing.Mapping[str, typing.Union[str, int]])
{'key': 'value', 'quantity': 5}
>>> validobj.parse_input({'key': 'value', 'quantity': 5}, typing.Mapping[str, str]) 
Traceback (most recent call last):
...
validobj.errors.WrongTypeError: Expecting value of type 'str', not int.

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
...
validobj.errors.WrongFieldError: Cannot process value for key 'quantity'

Enums

Strings can be automatically converted to valid enum.Enum elements:

>>> import enum
>>> class Colors(enum.Enum):
...     RED = enum.auto()
...     GREEN = enum.auto()
...     BLUE = enum.auto()
...
>>> validobj.parse_input('RED', Colors)
<Colors.RED: 1>
>>> validobj.parse_input('NORED', Colors) 
Traceback (most recent call last):
...
validobj.errors.NotAnEnumItemError: 'NORED' is not a valid member of 'Colors'. Alternatives to invalid value 'NORED' include:
  - RED
All valid values are:
  - RED
  - GREEN
  - BLUE

Additionally lists of strings can be turned into instances of enum.Flag:

>>> class Permissions(enum.Flag):
...     READ = enum.auto()
...     WRITE = enum.auto()
...     EXECUTE = enum.auto()
...
>>> validobj.parse_input('READ', Permissions)
<Permissions.READ: 1>
>>> validobj.parse_input(['READ', 'EXECUTE'], Permissions)
<Permissions.EXECUTE|READ: 5>
>>> validobj.parse_input([], Permissions)
<Permissions.0: 0>

Dataclasses

The dataclasses module is supported and input is parsed based on the type annotations:

>>> import dataclasses
>>> @dataclasses.dataclass
... class FileMeta:
...     description: str = ""
...     keywords: typing.List[str] = dataclasses.field(default_factory=list)
...     author: str = ""
>>> @dataclasses.dataclass
... class File:
...     location: str
...     meta: FileMeta = dataclasses.field(default_factory=FileMeta)
...     storage_class: dataclasses.InitVar[str] = "local"
>>> validobj.parse_input({'location': 'https://example.com/file', 'storage_class': 'remote'}, File)
File(location='https://example.com/file', meta=FileMeta(description='', keywords=[], author=''))

Fields with defaults (or default factories) are inferred. Fields that are themselves dataclasses are processed recursively. Init-only variables using dataclasses.InitVar are supported, with the types checked.

Rich tracebacks are produced in case of validation error:

>>> validobj.parse_input({'location': 'https://example.com/file', 'meta':{'keywords': [1, 'x', 'xx']}}, File) 
Traceback (most recent call last):
...
validobj.errors.WrongTypeError: Expecting value of type 'str', not int.

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
...
validobj.errors.WrongListItemError: Cannot process list item 1.

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
...
validobj.errors.WrongFieldError: Cannot process field 'keywords' of value into the corresponding field of 'FileMeta'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
...
validobj.errors.WrongFieldError: Cannot process field 'meta' of value into the corresponding field of 'File'