|
11 | 11 | from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Mapping, MutableMapping, Tuple, cast |
12 | 12 |
|
13 | 13 | import jsonref |
14 | | -from jsonschema import validate |
| 14 | +from jsonschema import Draft7Validator |
15 | 15 | from jsonschema.exceptions import ValidationError |
16 | 16 | from pydantic.v1 import BaseModel, Field |
17 | 17 | from referencing import Registry, Resource |
@@ -185,25 +185,66 @@ def _resolve_schema_references(self, raw_schema: dict[str, Any]) -> dict[str, An |
185 | 185 | raise ValueError(f"Expected resolved to be a dict. Got {resolved}") |
186 | 186 |
|
187 | 187 |
|
| 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 | + |
188 | 212 | def check_config_against_spec_or_exit( |
189 | 213 | config: Mapping[str, Any], spec: ConnectorSpecification |
190 | 214 | ) -> 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. |
194 | 216 |
|
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. |
197 | 220 | """ |
198 | 221 | 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 |
207 | 248 |
|
208 | 249 |
|
209 | 250 | class InternalConfig(BaseModel): |
|
0 commit comments