path-to-error

A Serde Deserializer wrapper that reports the exact field path where a deserialization error occurred.

Library
Cargo
v0.1.20
427stars
Apache License 2.0

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
46/100Fair
Development Activity32
Maintenance28
Community44
Maturity60
Momentum20

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
79/100Good
Architecture85
Code Quality82
Innovation68
Learning Curve80

serde_path_to_error solves a specific pain point in Rust’s Serde ecosystem: when deserialization fails on a large or deeply nested structure, the default error only names the type-level failure (“invalid type: integer, expected a string”) with no indication of where in the document it happened. This crate wraps any existing Serde Deserializer — JSON, YAML, TOML, or otherwise — and threads a chain of field names, sequence indices, and enum variants through every nested call, so a failed deserialize returns both the original error and a dotted path like dependencies.serde.version.

It is a tiny, dependency-light crate (itoa plus serde_core) built to be dropped into any existing Deserialize call with minimal code change: swap T::deserialize(deserializer) for serde_path_to_error::deserialize(deserializer) and call .path() on the error. It also supports the mirror case for serialization errors via a Serializer wrapper. Maintained by dtolnay (also the author of serde, syn, and anyhow), it is #![no_std]-compatible and has shipped over 260 million downloads on crates.io, making it a de facto standard for surfacing actionable deserialization diagnostics in config loaders, API request validators, and CLI tools built on Serde.

What You Get

  • A deserialize() function that wraps any Deserializer and returns an Error<E> carrying both the original error and a resolved Path
  • A Path type with Display (dot-separated string like a.b[2].c) and an iter() over individual Segments (map keys, sequence indices, enum variants, or unknown)
  • A mirrored Serializer wrapper and serialize() function for locating the source of serialization failures
  • A low-level Track/Deserializer API for manually instrumented deserializers that don’t go through the top-level deserialize() entry point
  • #![no_std] support (via alloc), so it works in embedded and constrained environments that already use Serde

Common Use Cases

  • Reporting precisely which field in a large JSON/YAML/TOML config file failed to parse, instead of a bare type mismatch
  • Improving CLI and web API error messages for user-submitted structured input (request bodies, config files, manifests)
  • Debugging deeply nested or generated Deserialize structs during development, where the default serde_json error path is ambiguous
  • Building developer-facing tooling (linters, schema validators, config editors) that needs to highlight the exact offending key or array index

Under The Hood

Architecture The crate is organized around a Chain<'a> linked-list enum (src/lib.rs) representing the current position during deserialization — variants for Root, Seq, Map, Struct, Enum, Some, NewtypeStruct/NewtypeVariant, and NonStringKey — each borrowing its parent to avoid heap allocation while walking. A Track struct holds a Cell<Option<Path>> that gets set exactly once, on the first error encountered, via trigger()/trigger_impl(). The wrapping Deserializer (src/de.rs, the largest file at ~1500 lines) implements every Serde visitor callback, pushing a new Chain frame before recursing into nested values and calling Track::trigger when the inner deserializer returns an error. src/path.rs defines the public Path/Segment/Segments types that get built by walking the Chain backwards (Path::from_chain) once an error is known, reversing the collected segments into document order. src/ser.rs mirrors this design for the serialization direction, and src/wrap.rs provides shared plumbing between the two. The design cleanly separates concerns: Chain is an ephemeral, allocation-free traversal structure used during the walk, while Path is the owned, allocated result materialized only once, on failure.

Tech Stack Pure Rust, edition = "2021", minimum supported Rust version 1.71. Runtime dependencies are minimal: itoa (fast integer-to-string formatting for sequence index segments) and serde_core (the trait-only subset of Serde, used with default-features = false and alloc for #![no_std] compatibility). A cfg(any())-gated dependency on the full serde crate exists purely to keep Cargo.lock resolvable without ever being compiled in. Dev-dependencies (serde, serde_derive, serde_json) are used only for the test suite. CI (.github/workflows/ci.yml) runs the test matrix across nightly/beta/stable/MSRV Rust, a minimal-versions resolution check, and a documentation build, with RUSTFLAGS=-Dwarnings enforced throughout.

Code Quality The crate has a dedicated tests/ directory with deserialize.rs and serialize.rs integration tests exercising structs, vecs, maps, enums, nested combinations, and non-string map keys against serde_json, each asserting the exact resulting path string. Error handling is explicit and total — every Serde visitor method either delegates or records path context via Track, with no silent fallbacks. The crate carries an extensive, deliberate set of #![allow(clippy::...)] lint suppressions with inline justifications (several linking to known clippy false-positive issues), indicating disciplined, intentional lint management rather than blanket suppression. CI runs the full matrix with warnings-as-errors, plus a nightly run with randomized type layout to catch layout-dependent bugs.

What Makes It Unique Most Serde error-reporting solutions require pre-generating custom Deserialize impls or switching to a different deserialization crate entirely. serde_path_to_error instead works as a drop-in wrapper around any existing Deserializer implementation with a single call-site change, requiring no macros, no derive changes, and no format-specific integration — it works identically with serde_json, serde_yaml, toml, or a hand-written Deserializer. Its #![no_std] support and near-zero dependency footprint make it viable in contexts (embedded, constrained WASM) where heavier diagnostic tooling isn’t practical, and its allocation-free Chain traversal keeps the happy path cost close to zero.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search