Repository navigation
fix(serialization): preserve validation error messages when deserializing invalid components - #270
fede-kamel wants to merge 1 commit into
Conversation
|
LGTM. The fix to wrap validation error types in so messages are preserved is clean and targeted. 912 tests pass. Ready for maintainer review. |
fede-kamel
left a comment
There was a problem hiding this comment.
Clean fix for the deserialization error regression.
Root cause is clearly identified
InitErrorDetails(type=e.type, loc=e.loc, input=()) loses the error context that pydantic needs for types like value_error. Rebuilding without context causes PydanticCustomError to raise TypeError: ValueError: 'error' required in context, swallowing the real validation message.
Fix is minimal and correct
Wrapping (e.type, e.msg) in PydanticCustomError preserves both the error type and message together, so pydantic can reconstruct the line error without needing the original input context. The partial-construction path (from_partial_dict) is unchanged — good, that one already returns collected errors correctly.
Tests are thorough
All 4 deserialization paths are tested (from_dict, from_json, from_yaml, deserializer_from_dict), covering both custom validator errors (unreachable end node) and standard pydantic errors (extra IO properties, field type mismatches).
No blockers.
…zing invalid components When a component failed validation during Component.from_dict, from_json or from_yaml, PydanticComponentDeserializationPlugin.deserialize rebuilt the ValidationError from the collected error type and location only. Pydantic error types such as value_error, which is what every @model_validator_with_error_accumulation validator raises, cannot be rebuilt without the context of the original error, so the deserializer crashed with "TypeError: ValueError: 'error' required in context" and the real message, for instance the end node without incoming control flow edge, was lost. The rebuilt line errors now wrap the original type and message in a PydanticCustomError, which keeps the error type and the message for every error type. The partial construction path (build_from_partial_config, get_validation_errors) is unchanged. Fixes oracle#269.
3e73dd2 to
6107693
Compare
fede-kamel
left a comment
There was a problem hiding this comment.
Review
The fix correctly addresses the root cause: Pydantic error types like value_error cannot be reconstructed from their type alone — they need context to render their message.
Key observations:
-
PydanticCustomError(e.type, e.msg)— wrapping the type and message preserves both without needing the original error context. This is the right approach for@model_validator_with_error_accumulationvalidators. -
Tests cover all code paths:
test_deserialization_reports_custom_validation_errorsverifies the flow validation case (unreachable end node) acrossfrom_dict,from_json,from_yaml, andDeserializer.from_dict.test_deserialization_reports_extra_io_property_errorschecks the Agent I/O property validation error.test_deserialization_reports_field_type_errorsconfirms standard Pydantic type errors still work.
-
Changelog entry is clear and actionable for end users.
One question: The PydanticCustomError preserves e.type and e.msg, but PydanticCustomError.__init__ expects (type: str, message: str). Are there any error types where e.type might not be a simple string, or e.msg could be None? Looking at the Pydantic source, ValidationError.error is ErrorDetails which has type: str and msg: str, so this should be safe.
Minor nit: The changelog says @model_validator_with_error_accumulation — this is a pyagentspec decorator, not a Pydantic builtin. Good to mention that this is a custom pyagentspec validator.
Overall, clean fix with thorough test coverage.
Fixes #269.
Root cause
PydanticComponentDeserializationPlugin.deserializecollects the validation errors of a component and rebuilds aValidationErrorfromInitErrorDetails(type=e.type, loc=e.loc, input=()). pydantic cannot render error types such asvalue_error, which is what every@model_validator_with_error_accumulationvalidator raises, from the type alone: they need the context of the original error.ValidationError.from_exception_datatherefore raisedTypeError: ValueError: 'error' required in contextand the real message (the unreachable end node, the input missing from the prompt, ...) was lost for everyfrom_dict,from_jsonandfrom_yamlcall on an invalid component.Changes
serialization/pydanticdeserializationplugin.py: wrap the original error type and message in aPydanticCustomErrorwhen rebuilding the line errors. This preserves both for every error type without needing the original context. The partial-construction path (from_partial_dict, which returns the collected errors) is unchanged.tests/serialization/test_serialization.py: regression tests covering aFlowwith an unreachableEndNodethroughComponent.from_dict,from_json,from_yamlandAgentSpecDeserializer.from_dict, and anAgentdeclaring an input that is not in its prompt. They assert theValidationErrorcarries the original message and thevalue_errortype. All fail onmainwith theTypeError, pass with the fix.Verification
SKIP_LLM_TESTS=1 pytest tests --ignore=tests/adapters --ignore=tests/evaluation: 912 passed, 407 skipped (Python 3.13).