Input and output

validobj.validation.parse_input() takes an input raw object, to be processed, and an output type specification specification to process the object into.

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. Users can add customizations for these as appropriate for their application.

Supported output

The input is coerced into a wider set of Python objects, as specified by the target specification.

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

Annotated

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'

Type aliases

Types can be defined as typing.TypeAliasType, using the type syntax in Python 3.12 onwards:

 >>> type MyType = str | tuple[str, str]
 >>> validobj.parse_input(["hi", "there"], MyType)
 ('hi', 'there')

NewType

typing.NewType works the same as if the type it wraps was given as input:

>>> MyNewType = typing.NewType("MyNewType", typing.Literal[5, 6])
>>> validobj.parse_input(5, MyNewType)
5

Typed mappings

typing.TypedDict is supported for Python versions newer 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'

NamedTuple

typing.NamedTuple (as well as the factory collections.namedtuple()) is supported, including annotations and default elements. The input to a tuple should be a list rather than a dict.

>>> import typing
>>> class Record(typing.NamedTuple):
...     uid: int
...     name: str
...     address: typing.Optional[str] = None
>>> validobj.parse_input([1, "Zah"], Record)
Record(uid=1, name='Zah', address=None)
>>> validobj.parse_input([1, "Zah", {"Address"}], Record)
Traceback (most recent call last):
...
WrongListItemError: Cannot process list item 3 into the field 'address' of 'Record'

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>

Note that enums are matched by name rather than by value. This allows for more natural support of enum.auto and enum.Flag.

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'