diff --git a/README.md b/README.md
index a4bb335..2868625 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,9 @@ builder.validate_field_value('server.ip', is_ipv4_address)
 builder.validate_field_value('server.port', is_unreserved_port)
 builder.validate_field_value('jwt.access_token_expires', is_timedelta)
 
+# you can also return custom error messages in your lambdas
+builder.validate_field_value('server.ip', lambda ip: (ip != "0.0.0.0", "IP is unroutable."))
+
 # parse a timedelta (e.g., Jun 1 2005) into a datetime object
 builder.transform_field_value('important_date', lambda date: datetime.strptime(date, '%b %d %Y'))
 builder.transform_field_value('jwt.access_token_expires', to_timedelta)
@@ -39,7 +42,10 @@ config = builder.parse_config('path/to/config.json')
 
 # access config values
 port = config.server.port
-assert port > 1024
+assert port > 1023
+
+ip = config.server.ip
+assert ip != "0.0.0.0"
 
 important_date = config.important_date
 assert isinstance(important_date, datetime)
diff --git a/python_json_config/config_builder.py b/python_json_config/config_builder.py
index ba76f6d..2e219b3 100644
--- a/python_json_config/config_builder.py
+++ b/python_json_config/config_builder.py
@@ -40,7 +40,14 @@ class ConfigBuilder(object):
     def __validate_field_values(self):
         for field_name, validation_function in self.__validation_functions.items():
             value = self.__config.get(field_name)
-            assert validation_function(value), f'Config field "{field_name}" contains invalid value "{value}"'
+            validation_result = validation_function(value)
+            error_message = f'Error validating field "{field_name}" with value "{value}"'
+
+            if isinstance(validation_result, tuple):
+                result, validation_error = validation_result
+                assert result, f"{error_message}: {validation_error}"
+            else:
+                assert validation_result, error_message
 
     def __transform_field_values(self):
         for field_name, transformation_function in self.__transformation_functions.items():
diff --git a/python_json_config/validators/generic_validators.py b/python_json_config/validators/generic_validators.py
index 4914b03..8fb28fa 100644
--- a/python_json_config/validators/generic_validators.py
+++ b/python_json_config/validators/generic_validators.py
@@ -1,4 +1,7 @@
-def is_timedelta(value: str) -> bool:
+from typing import Union, Tuple
+
+
+def is_timedelta(value: str) -> Union[bool, Tuple[bool, str]]:
     """
     Tests if the given value is a valid timedelta specification.
     The timedelta needs to be specified as a colon separated string, e.g.: "0:0:23:00:00"
@@ -13,12 +16,12 @@ def is_timedelta(value: str) -> bool:
     """
     split_values = value.split(':')
     if len(split_values) > 5:
-        return False
+        return False, "Timedelta contains more than 5 elements."
 
     try:
         [int(element) for element in split_values]
     except ValueError:
-        return False
+        return False, "Timedelta contains non-integer elements."
 
     return True
 
@@ -29,7 +32,7 @@ def is_valid_choice(options):
     :return: A functions that takes a value and tests if it is within the specified choices. This function returns True
              if the value in the config is in the passed options.
     """
-    def validator(value):
-        return value in options
+    def validator(value) -> Tuple[bool, str]:
+        return value in options, f"Value is not contained in the options {options}"
 
     return validator
diff --git a/python_json_config/validators/network_validators.py b/python_json_config/validators/network_validators.py
index 6579ccf..eb3ca8e 100644
--- a/python_json_config/validators/network_validators.py
+++ b/python_json_config/validators/network_validators.py
@@ -1,7 +1,8 @@
 import socket
+from typing import Union, Tuple
 
 
-def is_ipv4_address(ip_address: str):
+def is_ipv4_address(ip_address: str) -> Union[bool, Tuple[bool, str]]:
     """
     Tests if the given value is a valid IPv4 address
     :param ip_address: The ip address that is tested.
@@ -11,13 +12,13 @@ def is_ipv4_address(ip_address: str):
         socket.inet_aton(ip_address)
         return True
     except socket.error:
-        return False
+        return False, "IP address is not a valid IPv4 address."
 
 
-def is_unreserved_port(port: int):
+def is_unreserved_port(port: int) -> Union[bool, Tuple[bool, str]]:
     """
     Tests if the given port is an unreserved port.
     :param port: The port that is tested.
     :return: True if the port is not reserved otherwise False.
     """
-    return port > 1023
+    return port > 1023, "Port is reserved."

Better error reporting
There is currently no way to specify what exactly failed while parsing the config. In the optimal case, every validator should be able to define a message to show the user what exactly went wrong.

For example: `"There was an error while validating the host field, 127.0.0." is not a valid IP address."`
diff --git a/tests/config_builder_test.py b/tests/config_builder_test.py
index e4e516e..ec267b2 100644
--- a/tests/config_builder_test.py
+++ b/tests/config_builder_test.py
@@ -1,6 +1,7 @@
 from unittest import TestCase
 
 from python_json_config import ConfigBuilder
+from python_json_config.validators import is_unreserved_port
 
 
 class ConfigBuilderTest(TestCase):
@@ -52,7 +53,8 @@ class ConfigBuilderTest(TestCase):
         builder.validate_field_type('server.port', int)
         builder.parse_config(self.path)
         builder.validate_field_type('server.host', int)
-        with self.assertRaises(AssertionError): builder.parse_config(self.path)
+        with self.assertRaises(AssertionError):
+            builder.parse_config(self.path)
 
     def test_value_validation(self):
         builder = ConfigBuilder()
@@ -60,7 +62,8 @@ class ConfigBuilderTest(TestCase):
         builder.validate_field_value('server.port', lambda x: x < 10000)
         builder.parse_config(self.path)
         builder.validate_field_value('cache.ttl', lambda x: x > 200)
-        with self.assertRaises(AssertionError): builder.parse_config(self.path)
+        with self.assertRaises(AssertionError):
+            builder.parse_config(self.path)
 
     def test_value_transformation(self):
         builder = ConfigBuilder()
@@ -74,4 +77,9 @@ class ConfigBuilderTest(TestCase):
         self.assertTrue(config.server.debug_mode)
         self.assertEqual(config.server.port, 5004)
 
-
+    def test_value_validation_error_messages(self):
+        builder = ConfigBuilder()
+        builder.validate_field_value('server.debug_mode', lambda x: (x, "test error"))
+        with self.assertRaises(AssertionError,
+                               msg='Error validating field "server.debug_mode" with value "False": test error'):
+            builder.parse_config(self.path)
diff --git a/tests/config_node_test.py b/tests/config_node_test.py
index 0dc62b6..27f0e67 100644
--- a/tests/config_node_test.py
+++ b/tests/config_node_test.py
@@ -33,7 +33,8 @@ class ConfigNodeTest(TestCase):
     def test_get(self):
         node = ConfigNode(self.config_dict)
         self.assertEqual(node.key1, 1)
-        with self.assertRaises(AttributeError): node.nokey
+        with self.assertRaises(AttributeError):
+            node.nokey
 
         self.assertEqual(node.get('key1'), 1)
         with self.assertRaises(AttributeError): node.get('nokey')
@@ -50,7 +51,8 @@ class ConfigNodeTest(TestCase):
         self.assertIsInstance(node.key1, ConfigNode)
         self.assertEqual(node.key1.newkey, 1)
 
-        with self.assertRaises(AttributeError): node.key3
+        with self.assertRaises(AttributeError):
+            node.key3
         node.update('key3', 'asd')
         self.assertEqual(node.key3, 'asd')
 
diff --git a/tests/validators/generic_validators_test.py b/tests/validators/generic_validators_test.py
index ca3a93c..0285750 100644
--- a/tests/validators/generic_validators_test.py
+++ b/tests/validators/generic_validators_test.py
@@ -6,24 +6,27 @@ from python_json_config.validators import is_timedelta, is_valid_choice
 class GenericValidatorsTest(TestCase):
 
     def test_is_timedelta(self):
-        valid_timedeltas = ["1:3:24:30:23", "0:0:1", "0:0:0:0:1", "01:02:02:03:04"]
-        invalid_timedeltas = ["1:3:24:30:23:45", "01:a:02:03:04"]
+        valid_timedeltas = ['1:3:24:30:23', '0:0:1', '0:0:0:0:1', '01:02:02:03:04']
+        invalid_timedeltas = ['1:3:24:30:23:45', '01:a:02:03:04']
 
         for timedelta in valid_timedeltas:
             self.assertTrue(is_timedelta(timedelta))
 
-        for timedelta in invalid_timedeltas:
-            self.assertFalse(is_timedelta(timedelta))
+        self.assertEqual(is_timedelta(invalid_timedeltas[0]), (False, 'Timedelta contains more than 5 elements.'))
+        self.assertEqual(is_timedelta(invalid_timedeltas[1]), (False, 'Timedelta contains non-integer elements.'))
+
 
     def test_is_valid_choice(self):
-        list_validator = is_valid_choice([1, 2, "3"])
+        list_options = [1, 2, '3']
+        list_validator = is_valid_choice(list_options)
         self.assertTrue(list_validator(1))
-        self.assertFalse(list_validator(3))
-        self.assertTrue(list_validator("3"))
-        self.assertFalse(list_validator(4))
+        self.assertEqual(list_validator(3), (False, f'Value is not contained in the options {list_options}'))
+        self.assertTrue(list_validator('3'))
+        self.assertEqual(list_validator(4), (False, f'Value is not contained in the options {list_options}'))
 
-        dict_validator = is_valid_choice({1: "2", "3": 4})
+        dict_options = {1: '2', '3': 4}
+        dict_validator = is_valid_choice(dict_options)
         self.assertTrue(dict_validator(1))
-        self.assertFalse(dict_validator("2"))
-        self.assertTrue(dict_validator("3"))
-        self.assertFalse(dict_validator(4))
+        self.assertEqual(dict_validator('2'), (False, f'Value is not contained in the options {dict_options}'))
+        self.assertTrue(dict_validator('3'))
+        self.assertEqual(dict_validator(4), (False, f'Value is not contained in the options {dict_options}'))
diff --git a/tests/validators/network_validators_test.py b/tests/validators/network_validators_test.py
index 575e690..df6be47 100644
--- a/tests/validators/network_validators_test.py
+++ b/tests/validators/network_validators_test.py
@@ -6,19 +6,19 @@ from python_json_config.validators import is_ipv4_address, is_unreserved_port
 class NetworkValidatorsTest(TestCase):
 
     def test_is_ipv4_address(self):
-        valid_ips = ["127.0.0.1", "8.8.8.8", "127.1", "8.526344"]
-        invalid_ips = ["327.0.0.1", "8.8.8.8.8", "127.-1", "256.526344"]
+        valid_ips = ['127.0.0.1', '8.8.8.8', '127.1', '8.526344']
+        invalid_ips = ['327.0.0.1', '8.8.8.8.8', '127.-1', '256.526344']
 
         for address in valid_ips:
             self.assertTrue(is_ipv4_address(address))
 
         for address in invalid_ips:
-            self.assertFalse(is_ipv4_address(address))
+            self.assertEqual(is_ipv4_address(address), (False, 'IP address is not a valid IPv4 address.'))
 
     def test_is_unreserved_port(self):
-        self.assertFalse(is_unreserved_port(1))
-        self.assertFalse(is_unreserved_port(-1))
-        self.assertFalse(is_unreserved_port(22))
-        self.assertFalse(is_unreserved_port(1023))
+        self.assertEqual(is_unreserved_port(1), (False, 'Port is reserved.'))
+        self.assertEqual(is_unreserved_port(-1), (False, 'Port is reserved.'))
+        self.assertEqual(is_unreserved_port(22), (False, 'Port is reserved.'))
+        self.assertEqual(is_unreserved_port(1023), (False, 'Port is reserved.'))
         self.assertTrue(is_unreserved_port(1024))
         self.assertTrue(is_unreserved_port(14302))

pip install -e .[dev]
parse_log_pytest
pytest
pytest
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
packaging @ file:///croot/packaging_1734472117206/work
pluggy @ file:///croot/pluggy_1733169602837/work
pytest @ file:///croot/pytest_1738938843180/work
-e git+https://github.com/janehmueller/python-json-config.git@3b146dd2fb8d66007fbb2f2922f7fdcfdcfb21d4#egg=python_json_config
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work

name: python-json-config
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
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_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=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
prefix: /opt/conda/envs/python-json-config

tests/config_builder_test.py::ConfigBuilderTest::test_value_validation_error_messages tests/validators/generic_validators_test.py::GenericValidatorsTest::test_is_timedelta tests/validators/generic_validators_test.py::GenericValidatorsTest::test_is_valid_choice tests/validators/network_validators_test.py::NetworkValidatorsTest::test_is_ipv4_address tests/validators/network_validators_test.py::NetworkValidatorsTest::test_is_unreserved_port
tests/config_builder_test.py::ConfigBuilderTest::test_parse tests/config_builder_test.py::ConfigBuilderTest::test_type_validation tests/config_builder_test.py::ConfigBuilderTest::test_type_validation_builder tests/config_builder_test.py::ConfigBuilderTest::test_value_transformation tests/config_builder_test.py::ConfigBuilderTest::test_value_transformation_builder tests/config_builder_test.py::ConfigBuilderTest::test_value_validation tests/config_builder_test.py::ConfigBuilderTest::test_value_validation_builder tests/config_node_test.py::ConfigNodeTest::test_contains tests/config_node_test.py::ConfigNodeTest::test_creation tests/config_node_test.py::ConfigNodeTest::test_get tests/config_node_test.py::ConfigNodeTest::test_pickle tests/config_node_test.py::ConfigNodeTest::test_string tests/config_node_test.py::ConfigNodeTest::test_update
python_json_config/validators/generic_validators.py README.md python_json_config/validators/network_validators.py python_json_config/config_builder.py
python_json_config/validators/generic_validators.py README.md python_json_config/validators/network_validators.py python_json_config/config_builder.py