Skip to content

feat(sdk): expose decode path and kind on JSON decode failures - #370

Merged
benbenbang merged 8 commits into
mainfrom
feat/366-decode-path
Sep 29, 2026
Merged

benbenbang merged 8 commits into
mainfrom
feat/366-decode-path

Conversation

@benbenbang

Copy link
Copy Markdown
Member

When a JSON response fails to decode, callers previously received a generic error that neither located the offending member nor explained why it was rejected. This made debugging decode failures difficult while offering no safe way to inspect them, since surfacing the raw decoder message risked leaking response values. This change adds structured, redaction-safe decode diagnostics consistently across the Rust, Python, and Go SDKs.

What changed:

  • Added decode diagnostics to the core library, decoding via serde_path_to_error and introducing a coarse, non-exhaustive DecodeErrorKind (syntax, null, missing_member, wrong_type, invalid_value) with stable lowercase string values, plus decode_path and decode_kind accessors on Error. The decoder's own message is discarded so no member value survives, and paths are redacted before reaching the caller.
  • Surfaced the same information in the Python SDK as decode_path and decode_kind attributes on FmpError, and in the Go SDK as Path (an RFC 6901 JSON pointer) and DecodeKind fields on Error, read only from structured fields and never from error text.
  • Classified value-level failures (such as a non-integral count) in the Go SDK as invalid-value rather than a generic wrong-type error, so such members report their own kind and location.
  • Documented that DecodeKind values are stable and only ever appended, so consumers can depend on existing values.

Other:

  • Added serde_path_to_error as a workspace dependency and wired it into libfmp.
  • Added a synthetic null-beta company-screener fixture and extended decode-path test suites across all three SDKs, covering member-level failures, invalid values, and non-JSON or empty bodies reported as root-level syntax failures, while asserting that offending values never leak into any message, repr, or attribute.

Refs #366

Introduce the `serde_path_to_error` crate as a workspace dependency and
wire it into the libfmp crate to support decode path reporting.

Refs #366
Synthetic 40-row company-screener fixture whose row 37 carries beta null, for the decode-path acceptance tests (issue 366)
Decode failures on *fmp.Error now locate the offending member without
ever exposing its value. The Error struct gains a Path field holding the
RFC 6901 JSON pointer of the failing member (such as "/37/beta") and a
DecodeKind field giving a coarse, redaction-safe reason for the failure.
Path is "" for a failure at the document root and on every non-decode
error.

DecodeKind mirrors the Rust crate's DecodeErrorKind with the stable
lowercase values syntax, null, missing_member, wrong_type, and
invalid_value, so the reason stays consistent across the Rust, Python,
and Go SDKs. Error formatting now appends the decode kind and path.

The client reads only the structured fields of the json/v2 error, never
its text, and consults the body to distinguish a JSON null from an
absent member. The pointer is passed through the redactor before it
reaches the caller, so no member value survives on the error.
Decode JSON responses through serde_path_to_error so failures can report
where and why they occurred without leaking the offending value.

**Decode diagnostics on Error:**
* Added `DecodeErrorKind` in `error.rs`, a coarse, non-exhaustive
  classification (`syntax`, `null`, `missing_member`, `wrong_type`,
  `invalid_value`) with a stable lowercase `as_str` for language bindings.
* Added `Error::decode_path` and `Error::decode_kind` accessors, plus the
  internal `with_decode_location` builder, and extended the `Display`
  output to include the kind description and member path.
* The path names members and array indexes only, for example `[37].beta`,
  and is `None` for root-level and non-JSON failures such as binary bodies.

**JSON decoding:**
* `decode_json` now deserializes via `serde_path_to_error`, classifies the
  failure into a `DecodeFailure` carrying the path and kind, and discards
  the decoder's own message so no member value survives.
* The client redacts the decode path before attaching it to the error.
Surface where and why a JSON response failed to decode without leaking
the offending member value. `FmpError` now carries two new attributes:
`decode_path`, naming the member (for example `[37].beta`), and
`decode_kind`, giving the reason (`syntax`, `null`, `missing_member`,
`wrong_type`, or `invalid_value`).

These attributes let callers locate a decode failure precisely while
keeping response contents out of both the message and the error object.
Tests confirm the path and kind are reported and that the offending
string value never reaches the message, repr, or any attribute.

close #366
Extend the decode path test suites across the Python, Rust, and Go SDKs
to verify that non-JSON and empty response bodies surface as root-level
syntax failures with no decode path.

* Python: assert the raised `FmpDecodeError` reports a `syntax` kind and
  `None` path for a non-JSON body.
* Rust: add a case covering both non-JSON and empty bodies as root
  syntax failures.
* Go: add a `not json` case expecting an empty path and `DecodeKindSyntax`.
Clarify that DecodeKind values are stable and that new kinds are only
appended, so consumers can rely on existing values not changing.
The Go SDK previously reported a fractional count and other value-level
decode failures as a generic wrong-type error, so callers could not tell
where in the response the rejected value lived or why it was rejected.

**Decode error classification**:
* Added `invalidMemberValueError` in `sdk/go/decode.go`, which marks a
  member whose JSON kind is correct but whose value the Rust decoder
  rejects, such as a non-integral count.
* Tracked the new `memberKind` on the error and surfaced it through the
  decode path/kind resolution so such members report their own kind
  instead of defaulting to `DecodeKindWrongType`.

**Test coverage**:
* Extended the Go, Rust, and Python analyst tests to assert the decode
  path (`/0/buy`, `[0].buy`), the `InvalidValue` kind, and that the
  member value is never leaked into the error message.
@benbenbang
benbenbang enabled auto-merge (rebase) September 29, 2026 03:05
@github-actions github-actions Bot added documentation Improvements or additions to documentation enhancement New feature or request build labels Sep 29, 2026
@benbenbang
benbenbang merged commit fd590e5 into main Sep 29, 2026
9 checks passed
@benbenbang
benbenbang deleted the feat/366-decode-path branch September 29, 2026 03:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant