Skip to content

Commit cf497c0

Browse files
devin-ai-integration[bot]bot_apk
andcommitted
fix: improve oneOf/anyOf config validation error messages
Replace validate() with Draft7Validator.iter_errors() to detect oneOf/anyOf validation failures and produce per-branch error details instead of surfacing a misleading error from the last non-matching branch (e.g. "'Service' was expected" when the user intended OAuth). The improved message lists each oneOf option by title with the specific validation errors for that branch, making it actionable for users to identify which required fields are missing in their chosen auth method. Co-Authored-By: bot_apk <apk@cognition.ai>
1 parent 69cd63d commit cf497c0

1 file changed

Lines changed: 55 additions & 14 deletions

File tree

airbyte_cdk/sources/utils/schema_helpers.py

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Mapping, MutableMapping, Tuple, cast
1212

1313
import jsonref
14-
from jsonschema import validate
14+
from jsonschema import Draft7Validator
1515
from jsonschema.exceptions import ValidationError
1616
from pydantic.v1 import BaseModel, Field
1717
from referencing import Registry, Resource
@@ -185,25 +185,66 @@ def _resolve_schema_references(self, raw_schema: dict[str, Any]) -> dict[str, An
185185
raise ValueError(f"Expected resolved to be a dict. Got {resolved}")
186186

187187

188+
def _format_oneof_error(error: ValidationError) -> str:
189+
"""Format a oneOf/anyOf validation error with per-branch details."""
190+
field_path = ".".join(str(p) for p in error.absolute_path) if error.absolute_path else "root"
191+
branch_schemas = error.schema.get(error.validator, [])
192+
branch_messages: list[str] = []
193+
errors_by_branch: dict[int, list[str]] = {}
194+
for sub_error in error.context:
195+
branch_index = sub_error.schema_path[0]
196+
if isinstance(branch_index, int):
197+
errors_by_branch.setdefault(branch_index, []).append(sub_error.message)
198+
199+
for i, branch_schema in enumerate(branch_schemas):
200+
label = branch_schema.get("title", f"option {i + 1}")
201+
sub_errors = errors_by_branch.get(i, [])
202+
if sub_errors:
203+
details = "; ".join(sub_errors)
204+
branch_messages.append(f"{label}: {details}")
205+
else:
206+
branch_messages.append(f"{label}: unknown validation failure")
207+
208+
joined = " | ".join(branch_messages)
209+
return f'Config validation error: the provided configuration for "{field_path}" does not match any supported option. {joined}.'
210+
211+
188212
def check_config_against_spec_or_exit(
189213
config: Mapping[str, Any], spec: ConnectorSpecification
190214
) -> None:
191-
"""
192-
Check config object against spec. In case of spec is invalid, throws
193-
an exception with validation error description.
215+
"""Check config object against spec and raise on validation failure.
194216
195-
:param config - config loaded from file specified over command line
196-
:param spec - spec object generated by connector
217+
Uses iter_errors to detect oneOf/anyOf failures and produce a message
218+
that lists per-branch validation details instead of surfacing a
219+
misleading error from the last non-matching branch.
197220
"""
198221
spec_schema = spec.connectionSpecification
199-
try:
200-
validate(instance=config, schema=spec_schema)
201-
except ValidationError as validation_error:
202-
raise AirbyteTracedException(
203-
message="Config validation error: " + validation_error.message,
204-
internal_message=validation_error.message,
205-
failure_type=FailureType.config_error,
206-
) from None # required to prevent logging config secrets from the ValidationError's stacktrace
222+
validator = Draft7Validator(spec_schema)
223+
errors = list(validator.iter_errors(config))
224+
if not errors:
225+
return
226+
227+
oneof_errors = [e for e in errors if e.validator in ("oneOf", "anyOf") and e.context]
228+
if oneof_errors:
229+
error = oneof_errors[0]
230+
message = _format_oneof_error(error)
231+
internal_parts = [
232+
f"{sub.message} (path: {list(sub.absolute_path)})" for sub in error.context
233+
]
234+
internal_message = (
235+
f"oneOf validation failed at path {list(error.absolute_path)}: "
236+
+ "; ".join(internal_parts)
237+
)
238+
else:
239+
error = errors[0]
240+
message = "Config validation error: " + error.message
241+
internal_message = error.message
242+
243+
raise AirbyteTracedException(
244+
message=message,
245+
internal_message=internal_message,
246+
failure_type=FailureType.config_error,
247+
) from None # required to prevent logging config secrets from the ValidationError's stacktrace
207248

208249

209250
class InternalConfig(BaseModel):

0 commit comments

Comments
 (0)