Skip to content

fix(react): detect job-attachment paths across all union branches#952

Draft
cotovanu-cristian wants to merge 1 commit into
mainfrom
fix/json-paths-union-branches
Draft

fix(react): detect job-attachment paths across all union branches#952
cotovanu-cristian wants to merge 1 commit into
mainfrom
fix/json-paths-union-branches

Conversation

@cotovanu-cristian

Copy link
Copy Markdown
Collaborator

Summary

get_json_paths_by_type is documented to return JSONPaths for all fields that reference a
given type. It silently misses every path when a field is a genuine multi-branch union and the
target type is not the first non-None member.

The cause is in _recursive_search: it called _unwrap_optional(annotation), which collapses any
Union to its first non-None member. So Union[str, Target], Union[A, Target],
Optional[Union[A, Target]], list[Union[A, Target]], the PEP-604 form str | Target | None, and
a union whose member nests the target type all reduced to the first branch — the rest were never
searched, the function returned [], and downstream extract_values_by_paths silently dropped the
value (e.g. a job attachment).

This matters in normal operation: a JSON-schema field {"anyOf": [{"type": "string"}, {"$ref": ".../job-attachment"}]} becomes Union[str, <Attachment>, None], so a real attachment passed in a
string-or-attachment field was never resolved.

The contract

The function's own docstring: "Get JSONPath expressions for all fields that reference a
specific type … recursively traverses nested Pydantic models to find all paths."
A field
reachable via any union member references that type, so its path must be returned. Union/Optional
handling is intended and already tested (test_optional_field, test_pipe_union_field).

Root cause & fix

agent/react/json_utils.py — inside _recursive_search, search every non-None union member
(recursively, including a union nested as a list's inner type) instead of collapsing to the first.
A new _union_members helper returns the non-None members of a union, or [annotation] for a
non-union — so non-union fields behave byte-identically to before. _unwrap_optional is left
unchanged; it still serves Optional-stripping in the RootModel root-peel and _coerce_field,
which only ever see single-member optionals.

This is a single change in the shared producer through which all callers route
(get_job_attachment_paths is the only production caller), not a per-caller patch.

Tests

Added TestGetJsonPathsByTypeUnions covering second-branch unions, optional unions, PEP-604
three-member unions, list[Union[...]], a union member that nests the type, multiple branches
referencing the type, the first-branch baseline, and the dynamic anyOf/$ref case. Each fails
on the current code and passes with the fix. Full tests/agent/react + tests/agent/tools suite:
893 passed.

get_json_paths_by_type collapsed every field annotation to the first
non-None union member (via _unwrap_optional), so a target type reachable
through any other branch of an anyOf/oneOf union was never searched. A field
typed `str | Target`, `Union[A, B]`, `Optional[Union[...]]`, `list[Union[...]]`,
or a union whose member nests the type returned no path, and downstream
extract_values_by_paths silently dropped the value (e.g. a job attachment).

Search every non-None union member (recursively, including list-inner
unions) instead of collapsing to the first. _unwrap_optional is unchanged and
still serves Optional-stripping in the RootModel root-peel and _coerce_field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant