eltonscm: I think this also solves #23 because _"Optional[X] is equivalent to Union[X, None]"_ [0]. So, the following test should pass:

```python
@pytest.mark.parametrize('default', [1, None])
def test_model_validate_optional(default):
    class Foo(Model):
        optional: typing.Optional[int] = default

    foo = Foo()
    foo.validate()

    assert foo.optional == default
```

[0] https://docs.python.org/3/library/typing.html#typing.Optional
lamenezes: @daneoshiga thanks for your PR! I'll review it as soon as I can.
diff --git a/README.rst b/README.rst
index b61c4bd..39fd900 100644
--- a/README.rst
+++ b/README.rst
@@ -138,10 +138,14 @@ followed by the attribute name, e.g.
         def validate_age(self, age):
             if age < 0 or age > 150:
                 raise ValidationError('Invalid value for age {!r}'.format(age))
+            
+            return age
 
         def validate_height(self, height):
             if height <= 0:
                raise ValidationError('Invalid value for height {!r}'.format(age))
+            
+            return height
 
 
 Let's test it:
diff --git a/simple_model/fields.py b/simple_model/fields.py
index d189463..baf9720 100644
--- a/simple_model/fields.py
+++ b/simple_model/fields.py
@@ -1,4 +1,4 @@
-from typing import Any, List, Tuple, TypeVar
+from typing import Any, List, Union, Tuple, TypeVar
 
 from .exceptions import EmptyField
 
@@ -37,6 +37,13 @@ class ModelField:
         if not field_class or field_class is Any or value is None or self.is_property:
             return value
 
+        if field_class is Union:
+            assert issubclass(type(value), field_type.__args__), (
+                'Field of type {} received an object of invalid type {}').format(
+                    field_type.__args__, type(value))
+
+            return value
+
         if not issubclass(field_class, PARAMETRIZED_GENERICS) and type(value) is field_class:
             return value
 

Support fields with type typing.Union
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 9842f0b..0786759 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -128,6 +128,19 @@ def test_model_field_convert_to_type_unset(typeless_model_field):
     assert typeless_model_field.convert_to_type(None, value, field_class=typing.Any) is value
 
 
+@pytest.mark.parametrize('value', (1, '1'))
+def test_model_field_convert_to_type_union(typeless_model_field, value):
+    assert typeless_model_field.convert_to_type(None, value, field_class=typing.Union[int, str]) is value
+
+
+def test_model_field_convert_to_type_union_invalid(typeless_model_field):
+    value = dict()
+    with pytest.raises(AssertionError) as exc_info:
+        typeless_model_field.convert_to_type(None, value, field_class=typing.Union[int, str])
+
+    assert str(exc_info.value) == "Field of type (<class 'int'>, <class 'str'>) received an object of invalid type <class 'dict'>"
+
+
 def test_model_field_convert_to_type_value_has_correct_type(model_field):
     value = 'valuable'
 
diff --git a/tests/test_models.py b/tests/test_models.py
index 1ffe217..85d7f2c 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -427,6 +427,17 @@ def test_field_factory_model():
     assert model.string == 'foobar'
 
 
+@pytest.mark.parametrize('default', ('1', 1))
+def test_model_validate_union(default):
+    class UnionModel(Model):
+        union: typing.Union[int, str] = default
+
+    union_model = UnionModel()
+    union_model.validate()
+    assert union_model.union == default
+    assert type(union_model.union) == type(default)
+
+
 def test_model_validate_and_clean_invalid_mocked_model(model):
     model.validate = mock.Mock(side_effect=ValidationError)
 

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov mypy flake8
3.7
requirements-dev.txt requirements-test.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
Babel==2.14.0
backcall==0.2.0
certifi @ file:///croot/certifi_1671487769961/work/certifi
cfgv==3.3.1
charset-normalizer==3.4.1
coverage==7.2.7
decorator==5.1.1
distlib==0.3.9
docutils==0.17.1
exceptiongroup==1.2.2
filelock==3.12.2
flake8==5.0.4
identify==2.5.24
idna==3.10
imagesize==1.4.1
importlib-metadata==4.2.0
iniconfig==2.0.0
ipdb==0.13.13
ipython==7.34.0
jedi==0.19.2
Jinja2==3.1.6
MarkupSafe==2.1.5
matplotlib-inline==0.1.6
mccabe==0.7.0
mypy==1.4.1
mypy-extensions==1.0.0
nodeenv==1.9.1
packaging==24.0
parso==0.8.4
pexpect==4.9.0
pickleshare==0.7.5
platformdirs==2.6.2
pluggy==1.2.0
pre-commit==2.21.0
prompt_toolkit==3.0.48
ptyprocess==0.7.0
pycodestyle==2.9.1
pyflakes==2.5.0
Pygments==2.17.2
-e git+https://github.com/lamenezes/simple-model.git@95eaacb0da70364c1ee3aaf5e0d205f8e3255dfe#egg=pysimplemodel
pytest==7.4.4
pytest-cov==4.1.0
pytz==2025.2
PyYAML==6.0.1
requests==2.31.0
snowballstemmer==2.2.0
Sphinx==4.3.2
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
tomli==2.0.1
traitlets==5.9.0
typed-ast==1.5.5
typing_extensions==4.7.1
urllib3==2.0.7
virtualenv==20.16.2
wcwidth==0.2.13
zipp==3.15.0

name: simple-model
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - babel==2.14.0
      - backcall==0.2.0
      - cfgv==3.3.1
      - charset-normalizer==3.4.1
      - coverage==7.2.7
      - decorator==5.1.1
      - distlib==0.3.9
      - docutils==0.17.1
      - exceptiongroup==1.2.2
      - filelock==3.12.2
      - flake8==5.0.4
      - identify==2.5.24
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==4.2.0
      - iniconfig==2.0.0
      - ipdb==0.13.13
      - ipython==7.34.0
      - jedi==0.19.2
      - jinja2==3.1.6
      - markupsafe==2.1.5
      - matplotlib-inline==0.1.6
      - mccabe==0.7.0
      - mypy==1.4.1
      - mypy-extensions==1.0.0
      - nodeenv==1.9.1
      - packaging==24.0
      - parso==0.8.4
      - pexpect==4.9.0
      - pickleshare==0.7.5
      - platformdirs==2.6.2
      - pluggy==1.2.0
      - pre-commit==2.21.0
      - prompt-toolkit==3.0.48
      - ptyprocess==0.7.0
      - pycodestyle==2.9.1
      - pyflakes==2.5.0
      - pygments==2.17.2
      - pytest==7.4.4
      - pytest-cov==4.1.0
      - pytz==2025.2
      - pyyaml==6.0.1
      - requests==2.31.0
      - snowballstemmer==2.2.0
      - sphinx==4.3.2
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - tomli==2.0.1
      - traitlets==5.9.0
      - typed-ast==1.5.5
      - typing-extensions==4.7.1
      - urllib3==2.0.7
      - virtualenv==20.16.2
      - wcwidth==0.2.13
      - zipp==3.15.0
prefix: /opt/conda/envs/simple-model

tests/test_fields.py::test_model_field_convert_to_type_union[10] tests/test_fields.py::test_model_field_convert_to_type_union[11] tests/test_fields.py::test_model_field_convert_to_type_union_invalid tests/test_models.py::test_model_validate_union[10] tests/test_models.py::test_model_validate_union[11]
tests/test_models.py::test_model_fields_validate_allow_empty_error[None] tests/test_models.py::test_model_fields_validate_allow_empty_error[]
tests/test_fields.py::test_model_field tests/test_fields.py::test_empty_model_field_default_value tests/test_fields.py::test_model_field_to_python_simple[10] tests/test_fields.py::test_model_field_to_python_simple[11] tests/test_fields.py::test_model_field_to_python_simple[value2] tests/test_fields.py::test_model_field_to_python_simple[value3] tests/test_fields.py::test_model_field_to_python_nested tests/test_fields.py::test_model_field_to_python_nested_iterable[list] tests/test_fields.py::test_model_field_to_python_nested_iterable[tuple] tests/test_fields.py::test_model_field_to_python_iterable_empty[list] tests/test_fields.py::test_model_field_to_python_iterable_empty[tuple] tests/test_fields.py::test_model_field_to_python_iterable[list] tests/test_fields.py::test_model_field_to_python_iterable[tuple] tests/test_fields.py::test_model_field_validate tests/test_fields.py::test_model_field_validate_validation_error[ValidationError] tests/test_fields.py::test_model_field_validate_validation_error[EmptyField] tests/test_fields.py::test_model_field_validate_validation_error[MyValidationError] tests/test_fields.py::test_model_field_validate_empty_field[None] tests/test_fields.py::test_model_field_validate_empty_field[] tests/test_fields.py::test_model_field_validate_empty_field[blank_value2] tests/test_fields.py::test_model_field_validate_empty_field[blank_value3] tests/test_fields.py::test_model_field_validate_empty_field[blank_value4] tests/test_fields.py::test_model_field_validate_and_clean tests/test_fields.py::test_model_field_validate_and_clean_clean_nested tests/test_fields.py::test_model_field_convert_to_type_unset tests/test_fields.py::test_model_field_convert_to_type_value_has_correct_type tests/test_fields.py::test_model_field_convert_to_type_invalid_model_type tests/test_fields.py::test_model_field_convert_to_type_model_type tests/test_fields.py::test_model_field_convert_to_type tests/test_fields.py::test_model_field_convert_to_type_iterable_without_type[iterable_type0-list] tests/test_fields.py::test_model_field_convert_to_type_iterable_without_type[iterable_type1-tuple] tests/test_fields.py::test_model_field_convert_to_type_iterable_typed[iterable_type0-list] tests/test_fields.py::test_model_field_convert_to_type_iterable_typed[iterable_type1-tuple] tests/test_fields.py::test_model_field_convert_to_type_iterable_generic[list] tests/test_fields.py::test_model_field_convert_to_type_iterable_generic[tuple] tests/test_fields.py::test_model_field_convert_to_type_iterable_same_type[iterable_type0-list] tests/test_fields.py::test_model_field_convert_to_type_iterable_same_type[iterable_type1-tuple] tests/test_models.py::test_base_model tests/test_models.py::test_base_model_repr tests/test_models.py::test_base_model__get_fields tests/test_models.py::test_base_model_is_empty_false[False] tests/test_models.py::test_base_model_is_empty_false[0] tests/test_models.py::test_base_model_is_empty_false[foo] tests/test_models.py::test_base_model_is_empty_false[10] tests/test_models.py::test_base_model_is_empty_false[value4] tests/test_models.py::test_base_model_is_empty_false[value5] tests/test_models.py::test_base_model_is_empty_true[] tests/test_models.py::test_base_model_is_empty_true[value1] tests/test_models.py::test_base_model_is_empty_true[value2] tests/test_models.py::test_base_model_validate_and_clean tests/test_models.py::test_base_model_validate_fail tests/test_models.py::test_base_model___eq___equals tests/test_models.py::test_base_model___eq___not_equals tests/test_models.py::test_base_model___eq___not_equals_same_model tests/test_models.py::test_base_model___eq___not_equals_different_models_same_field_qty tests/test_models.py::test_model tests/test_models.py::test_model__get_fields tests/test_models.py::test_model_fields_without_fields tests/test_models.py::test_model_validate_empty[False] tests/test_models.py::test_model_validate_empty[0] tests/test_models.py::test_model_fields_field_validation tests/test_models.py::test_model_fields_field_validation_without_raise tests/test_models.py::test_model_fields_field_validation_error tests/test_models.py::test_model_fields_field_validation_error_without_raise tests/test_models.py::test_model_validate_nested tests/test_models.py::test_model_validate_and_clean_without_clean_method tests/test_models.py::test_model_validate_and_clean tests/test_models.py::test_model_validate_and_clean_nested tests/test_models.py::test_model_get_fields_invalid tests/test_models.py::test_build_many tests/test_models.py::test_build_many_empty_iterable tests/test_models.py::test_build_many_different_items tests/test_models.py::test_type_model tests/test_models.py::test_typed_model_type_conversion tests/test_models.py::test_model_inheritance_with_meta_fields tests/test_models.py::test_model_inheritance_without_meta_fields tests/test_models.py::test_typeless_model tests/test_models.py::test_field_factory_model tests/test_models.py::test_model_validate_and_clean_invalid_mocked_model tests/test_models.py::test_model_validate_and_clean_invalid_model_validate_called_after_clean tests/test_models.py::test_model_validate_and_clean_model_list tests/test_models.py::test_wtf_validate[] tests/test_models.py::test_wtf_validate[None] tests/test_models.py::test_model_validate_and_clean_type_conversion tests/test_models.py::test_field_conversion_model_type_conflict tests/test_models.py::test_field_conversion_list tests/test_models.py::test_model_property_getter tests/test_models.py::test_model_property_setter tests/test_models.py::test_model_property_setter_attribute_error
README.rst simple_model/fields.py
README.rst simple_model/fields.py