json-traits
Flatten a serde_json::Value into dotted leaf paths, diff two JSON values by those leaves, and match a path against patterns.
These paths are the same locations leaf-validator uses. That React library binds a control to a path such as person.contact.phoneNumber. Its leafDiff reports one entry per leaf:
location is the dotted path and updatedValue is the new leaf. leafDiff expands an object into one entry per leaf inside it. leaf-validator's diff can place a whole object in a single entry. This crate follows the leaf form.
MongoDB calls the same addressing dot notation: "contacts.2" is the third array element, and "person.contact.phoneNumber" is a field of an embedded document. JsonElementExtensions is the .NET library for the same operations.
Releases are published to crates.io. API docs are on docs.rs.
Add it to a project
The crate requires Rust 1.85 or newer. rustup installs the toolchain.
[]
= "0.1"
= "1"
To depend on the repository instead of the published crate:
[]
= { = "https://github.com/stewie1570/JsonTraits" }
= "1"
Bring the methods into scope with use json_traits::prelude::*;, or import JsonPaths, DiffJson, and PathPattern on their own. JsonScalar is the leaf type and is exported from the crate root.
| JsonElementExtensions | json-traits |
|---|---|
PathsAndValuesDictionary |
JsonPaths::paths_and_values |
DiffWith |
DiffJson::diff_with |
IsSupportedBy |
PathPattern::is_supported_by |
IsAPathMatchWith |
PathPattern::is_a_path_match_with |
paths_and_values
JsonPaths::paths_and_values walks a value and returns a BTreeMap<String, JsonScalar>. A leaf is a JSON string, number, boolean, or null. Objects and arrays are containers, so the walk continues through them. Object keys and array indexes become path segments. The map is sorted by path.
use BTreeMap;
use ;
use json;
let document = json!;
assert_eq!;
A value that is itself a string, number, boolean, or null is stored at the empty path "". An empty object or an empty array has no leaves, so it contributes no paths. Array indexes are decimal and unpadded: index 10 is the segment 10.
diff_with
DiffJson::diff_with compares those leaves. Paths that are equal on both sides are omitted. Each difference is a pair (left, right). JsonScalar::Undefined fills the side where the path is absent.
use BTreeMap;
use ;
use json;
let left = json!;
let right = json!;
assert_eq!;
contacts.0.info.name is absent from the result because both documents have "Stewie" there. The new leaf contacts.3.info.isSomething is (Undefined, true). A removed leaf is (value, Undefined). JSON null equals JSON null. A null leaf compared with a missing path is (Null, Undefined).
In JsonElementExtensions, DiffWith calls .Equals on the boxed value, and that call throws when the value is null. Here null is a leaf.
The same method is implemented for BTreeMap<String, JsonScalar>. left.paths_and_values().diff_with(&right.paths_and_values()) returns the map above.
Path patterns
is_supported_by is called on a path and takes the patterns to test. * matches one whole segment. contacts.*.info.name matches contacts.0.info.name. It leaves contacts.0.info.name.last unmatched, because that path has one more segment.
use PathPattern;
let paths = ;
let patterns = ;
let matched: = paths
.into_iter
.filter
.collect;
assert_eq!;
is_a_path_match_with is the same test called on the pattern:
use PathPattern;
assert!;
A pattern with no * matches that exact path. An empty pattern list matches nothing. The trait is implemented for str, so a String path works through deref.
JsonScalar
JsonScalar is one leaf:
| Variant | JSON |
|---|---|
Null |
null |
Bool |
true or false |
Number |
a JSON number |
String |
a JSON string |
Undefined |
the path is missing on this side of a diff |
JsonScalar::from_integer builds a number from an i64. JsonScalar::from_float builds one from a finite f64 and returns None for NaN and infinity. From is implemented for bool, &str, and String.
Integer 1 and decimal 1.0 compare equal. Two integers compare exactly, so a pair of large integers that would collapse to the same floating-point number stay different. A JSON string and a JSON number stay different when their text looks the same.
Behavior worth knowing
- Paths come out sorted, because the map is a
BTreeMap. - A dot inside an object key is copied into the path as written. The key
"a.b"and the nested object{"a": {"b": ...}}both become the patha.b. JsonElementExtensions and MongoDB dot notation share that overlap. - Empty containers contribute no leaves. Replacing a leaf with
{}or[]shows up as that leaf disappearing.
From a clone of this repository
list_paths reads a JSON document from standard input and prints every leaf path. rust-toolchain.toml selects the stable toolchain, including rustfmt and clippy, for commands run inside the repository.
License
MIT. See LICENSE.