Repository: meltano/sdk
Pull request #2778 (2024-11-27 16:56:20)

fix: The path of the offending field is now printed for config validation errors


<!-- readthedocs-preview meltano-sdk start -->
----
📚 Documentation preview 📚: https://meltano-sdk--2778.org.readthedocs.build/en/2778/

<!-- readthedocs-preview meltano-sdk end -->

codspeed-hq[bot]: ## [CodSpeed Performance Report](https://codspeed.io/meltano/sdk/branches/2777-bug-configuration-validation-error-messages-for-incorrect-types-dont-indicate-which-field-is-the-offender)

### Merging #2778 will **not alter performance**

<sub>Comparing <code>2777-bug-configuration-validation-error-messages-for-incorrect-types-dont-indicate-which-field-is-the-offender</code> (3c14117) with <code>main</code> (b425274)</sub>



### Summary

`✅ 6` untouched benchmarks  





Patch:
diff --git a/singer_sdk/plugin_base.py b/singer_sdk/plugin_base.py
index db16560d..f12f1522 100644
--- a/singer_sdk/plugin_base.py
+++ b/singer_sdk/plugin_base.py
@@ -38,6 +38,9 @@ from singer_sdk.typing import (
     extend_validator_with_defaults,
 )
 
+if t.TYPE_CHECKING:
+    from jsonschema import ValidationError
+
 SDK_PACKAGE_NAME = "singer_sdk"
 
 JSONSchemaValidator = extend_validator_with_defaults(DEFAULT_JSONSCHEMA_VALIDATOR)
@@ -88,6 +91,23 @@ class SingerCommand(click.Command):
             sys.exit(1)
 
 
+def _format_validation_error(error: ValidationError) -> str:
+    """Format a JSON Schema validation error.
+
+    Args:
+        error: A JSON Schema validation error.
+
+    Returns:
+        A formatted error message.
+    """
+    result = f"{error.message}"
+
+    if error.path:
+        result += f" in config[{']['.join(repr(index) for index in error.path)}]"
+
+    return result
+
+
 class PluginBase(metaclass=abc.ABCMeta):  # noqa: PLR0904
     """Abstract base class for taps."""
 
@@ -402,7 +422,9 @@ class PluginBase(metaclass=abc.ABCMeta):  # noqa: PLR0904
                 config_jsonschema,
             )
             validator = JSONSchemaValidator(config_jsonschema)
-            errors = [e.message for e in validator.iter_errors(self._config)]
+            errors = [
+                _format_validation_error(e) for e in validator.iter_errors(self._config)
+            ]
 
         if errors:
             summary = (


Test patch:
diff --git a/tests/core/conftest.py b/tests/core/conftest.py
index 30798b01..6fa9a5ff 100644
--- a/tests/core/conftest.py
+++ b/tests/core/conftest.py
@@ -11,8 +11,10 @@ from typing_extensions import override
 from singer_sdk import Stream, Tap
 from singer_sdk.helpers._compat import datetime_fromisoformat
 from singer_sdk.typing import (
+    ArrayType,
     DateTimeType,
     IntegerType,
+    ObjectType,
     PropertiesList,
     Property,
     StringType,
@@ -82,6 +84,22 @@ class SimpleTestTap(Tap):
         Property("username", StringType, required=True),
         Property("password", StringType, required=True),
         Property("start_date", DateTimeType),
+        Property(
+            "nested",
+            ObjectType(
+                Property("key", StringType, required=True),
+            ),
+            required=False,
+        ),
+        Property(
+            "array",
+            ArrayType(
+                ObjectType(
+                    Property("key", StringType, required=True),
+                ),
+            ),
+            required=False,
+        ),
         additional_properties=False,
     ).to_dict()
 
diff --git a/tests/core/test_tap_class.py b/tests/core/test_tap_class.py
index 93015fbb..48e02029 100644
--- a/tests/core/test_tap_class.py
+++ b/tests/core/test_tap_class.py
@@ -34,6 +34,30 @@ if t.TYPE_CHECKING:
             ["Additional properties are not allowed ('extra' was unexpected)"],
             id="extra_property",
         ),
+        pytest.param(
+            {"username": None, "password": "ptest"},
+            pytest.raises(ConfigValidationError, match="Config validation failed"),
+            ["None is not of type 'string' in config['username']"],
+            id="null_username",
+        ),
+        pytest.param(
+            {"username": "utest", "password": "ptest", "nested": {}},
+            pytest.raises(ConfigValidationError, match="Config validation failed"),
+            ["'key' is a required property in config['nested']"],
+            id="missing_required_nested_key",
+        ),
+        pytest.param(
+            {"username": "utest", "password": "ptest", "array": []},
+            nullcontext(),
+            [],
+            id="empty_array",
+        ),
+        pytest.param(
+            {"username": "utest", "password": "ptest", "array": [{}]},
+            pytest.raises(ConfigValidationError, match="Config validation failed"),
+            ["'key' is a required property in config['array'][0]"],
+            id="array_with_empty_object",
+        ),
         pytest.param(
             {"username": "utest", "password": "ptest"},
             nullcontext(),