Skip to content

indexed_at_path occasionally returned as a JSON-encoded string instead of parsed value #27

Description

@aural-psynapse

Summary

per_claim[i].indexed_at_path in the evaluate_handoff verdict is documented as the value extracted at the claim's json_path. Most of the time it's the parsed value (string, int, dict, list — whatever lives at the path). Occasionally it comes back as a JSON-encoded string of that value, e.g.:

indexed_at_path = '[{"created_at": "2026-04-12T15:30:00Z", "id": "ord_acme_002", ...}]'

— a string that contains the JSON, rather than the list itself.

Why this matters for consumers

Heal loops typically substitute indexed_at_path into the answer (heal_actions.py — see _short_primitive). When the leaf is a primitive it's safe to inline; when it's complex we skip. The string-encoded form fails that isinstance(leaf, (str, int, float)) check the wrong way: it looks primitive but is actually a JSON blob, so the substitution dumps a giant JSON string into the prose.

We patched it consumer-side by also rejecting strings that start with [ or { — but the heuristic is fragile, and it shouldn't be needed.

Proposed fix

indexed_at_path should always be the parsed value the SDK actually walked to. If the value happens to be a string, it should be a real string; if it's a list, a real list. The encoded shape suggests something like json.dumps is happening on a particular code path that should be reading the parsed structure directly.

If there's a deliberate reason (size limit, transport constraint), it should be:

  1. Documented in the verdict schema, and
  2. Consistently applied (always-string or always-parsed), so consumers don't have to type-sniff.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions