fix: round-trip serialization of typing.Annotated - #12339
Conversation
|
Someone is attempting to deploy a commit to the deepset Team on Vercel. A member of the Team first needs to authorize it. |
|
|
`serialize_type` had no handling for `typing.Annotated`, so it fell through to the generic-path logic and rendered the metadata values as if they were type names: ```python serialize_type(Annotated[int, 'doc']) # -> 'typing.Annotated[int, doc]' ``` `deserialize_type` then tried to resolve `doc` as a type and raised `DeserializationError`. A value that happened to look like a real type name silently corrupted instead of raising: `Annotated[str, 'int']` round-tripped to `Annotated[str, int]` (the string was read as the `int` type). The fix special-cases `typing.Annotated` on both sides, mirroring the recent `Literal` fix (PR deepset-ai#12286, commit 1f460e6): - **serialize:** render literal metadata (str/int/bool/None/bytes) with `repr()` so strings keep their quotes; render type-like metadata (classes, typing forms) through `serialize_type` so the full module path is preserved. - **deserialize:** split the type from the metadata on the first top-level comma — a quote-aware split, since a string metadata value can contain a comma (`Annotated[int, 'a, b']`). The type is resolved through `deserialize_type`; the metadata is parsed in bulk with `ast.literal_eval` (quote-aware, limited to safe literals) and falls back to `deserialize_type` per-arg when a type metadata is present. An `Annotated` type used by a component (e.g. an `OutputAdapter` whose output type is `Annotated[Result, 'computed']`, or any tool whose schema uses Pydantic `Field` metadata) now round-trips through `Pipeline.dumps()` / `Pipeline.loads()`. All other types are unaffected. Fixes deepset-ai#12338
4cc0902 to
f34cb88
Compare
|
Hi @Harsh23Kashyap, thanks a lot for your contribution! 🙏 We noticed that the Contributor License Agreement (CLA) check ( To get your PR reviewed, please sign the CLA via the link in the |
`_safe_get_origin` and `_strict_types_are_compatible` in `haystack.core.type_utils` did not unwrap `typing.Annotated`, and `_unwrap_all` in `haystack.core.super_component.utils` had the same gap. A component socket declared as `Annotated[int, "doc"]` was treated as a different type from `int` and could not be connected, even though `Annotated[T, m1, m2, ...]` is the same type as `T` for type-checking purposes — the metadata is just an annotation, not a type modifier. This made the previous fix in `f34cb882b` (round-trip serialization of `Annotated`) only half-useful: a `Pipeline` could now serialize a component with `Annotated` socket types, but the loaded pipeline would refuse to connect those sockets to anything else. Both checks now unwrap `Annotated` to the underlying type. A `GreedyVariadic` / `Variadic` (which is expressed as `Annotated[T, HAYSTACK_VARIADIC_ANNOTATION]`) is still detected by the existing `_is_variadic_type` check before the new unwrap fires, so Haystack's variadic semantics are unchanged. Added direct unit tests for both checkers (`test_annotated_same_as_wrapped_type_strict`, `test_safe_get_origin_unwraps_annotated`, and `test_annotated_type_compatibility` in super_component) covering the common case, nested `Annotated`, generic wrapped types, and `Annotated[Optional[int]]`. Updated the reno note to mention both gaps. Same shape as the `Literal` fix (PR deepset-ai#12286) and the `Annotated` serialization fix in `f34cb882b`: small, focused, existing-pattern reuse (`_unwrap_annotated` helper mirrors `_safe_get_origin`'s existing shape).
Additional changes (review-time follow-up)A paranoid maintainer review turned up a second gap that the original PR's "round-trip through Pipeline" claim needed but didn't cover: the Pipeline type checker rejected The same
New tests in Reno note updated to mention both gaps. |
Related Issues
Proposed Changes:
serialize_typehad no handling fortyping.Annotated, so it fell through to the generic-path logic and rendered the metadata values as if they were type names:deserialize_typethen tried to resolvedocas a type and raisedDeserializationError. A value that happened to look like a real type name silently corrupted instead of raising:Annotated[str, "int"]round-tripped toAnnotated[str, int](the string was read as theinttype).The fix special-cases
typing.Annotatedon both sides, mirroring the recentLiteralfix (PR #12286, commit 1f460e6):repr()so strings keep their quotes; render type-like metadata (classes, typing forms) throughserialize_typeso the full module path is preserved.Annotated[int, "a, b"]). The type is resolved throughdeserialize_type; the metadata is parsed in bulk withast.literal_eval(quote-aware, limited to safe literals) and falls back todeserialize_typeper-arg when a type metadata is present.Scope note: this covers the metadata kinds Python's own
Annotatedaccepts and that survive a text round-trip (str, bytes, int, bool, None — same set asLiteral). Type metadata (a class as a metadata value, e.g.Annotated[int, MyValidator]) is also handled via theserialize_type/deserialize_typefallback path. Enum members and arbitrary callables are out of scope — they cannot be reconstructed from text alone.How did you test it?
serialization,deserialization, andround_triptests forAnnotatedintest/utils/test_type_serialization.py, mirroring the existingLiteralandCallabletrios. Covers: failing case (Annotated[int, "doc"]), silent-corruption case (Annotated[str, "int"]), multiple metadata values, non-string literal metadata (int / bool / None / bytes), a comma inside a string metadata value (Annotated[int, "a, b"]),Ellipsismetadata, type-metadata fallback (Annotated[int, str]), typing-form metadata (Annotated[int, List[str]]),Literalas metadata, nested inList/Optional, and the wrapped type being aCallablewith a top-level comma in its parameter list._split_annotated_argshelper (quote-aware split, no comma / no metadata / nested brackets).typing.Annotated[int]form (no metadata) —DeserializationErrorrather than a silent misshape.200 passed(195 existing + 5 new), no existing tests broken. The broader serialization surface (test/core/test_serialization.py,test/core/test_serialization_security.py,test/core/test_type_utils.py,test/core/pipeline/,test/components/converters/,test/components/routers/) also passes — 1245 + 825 tests across the related modules, no regressions.Checklist
releasenotes/notes/)hatch run fmtandhatch run test:typeslocally — both clean