Skip to main content

sort_keys/
lib.rs

1//! # sort-keys — recursively sort the keys of a JSON object
2//!
3//! Produce a copy of a [`serde_json::Value`] with object keys sorted — useful for
4//! deterministic, stable output. A faithful Rust port of the widely-used
5//! [`sort-keys`](https://www.npmjs.com/package/sort-keys) npm package.
6//!
7//! ```
8//! use serde_json::json;
9//! use sort_keys::{sort_keys, sort_keys_with, Options};
10//!
11//! assert_eq!(sort_keys(&json!({ "c": 0, "a": 0, "b": 0 })), json!({ "a": 0, "b": 0, "c": 0 }));
12//!
13//! // Recurse into nested objects and arrays with `deep`:
14//! assert_eq!(
15//!     sort_keys_with(&json!({ "b": { "d": 0, "c": 0 }, "a": 0 }), &Options::new().deep(true)),
16//!     json!({ "a": 0, "b": { "c": 0, "d": 0 } })
17//! );
18//! ```
19//!
20//! By default keys are compared by UTF-16 code units (matching JavaScript's default sort).
21//! Use [`sort_keys_by`] for a custom comparator.
22
23#![forbid(unsafe_code)]
24#![doc(html_root_url = "https://docs.rs/sort-keys/0.1.0")]
25
26use core::cmp::Ordering;
27use serde_json::{Map, Value};
28
29// Compile-test the README's examples as part of `cargo test`.
30#[cfg(doctest)]
31#[doc = include_str!("../README.md")]
32struct ReadmeDoctests;
33
34/// Options controlling [`sort_keys_with`] / [`sort_keys_by`].
35#[derive(Debug, Clone, Default)]
36pub struct Options {
37    deep: bool,
38    ignore_keys: Vec<String>,
39}
40
41impl Options {
42    /// Default options (shallow, no ignored keys).
43    #[must_use]
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Recurse into nested objects and arrays.
49    #[must_use]
50    pub fn deep(mut self, value: bool) -> Self {
51        self.deep = value;
52        self
53    }
54
55    /// Keys to leave unsorted: they keep their original order and are placed before the
56    /// sorted keys.
57    #[must_use]
58    pub fn ignore_keys<I, S>(mut self, keys: I) -> Self
59    where
60        I: IntoIterator<Item = S>,
61        S: Into<String>,
62    {
63        self.ignore_keys = keys.into_iter().map(Into::into).collect();
64        self
65    }
66}
67
68/// Compare two strings by their UTF-16 code units (JavaScript's default string ordering).
69#[must_use]
70pub fn compare_utf16(a: &str, b: &str) -> Ordering {
71    a.encode_utf16().cmp(b.encode_utf16())
72}
73
74/// Sort the keys of `value` (shallow) using the default UTF-16 ordering.
75///
76/// ```
77/// # use serde_json::json;
78/// # use sort_keys::sort_keys;
79/// assert_eq!(sort_keys(&json!({ "b": 1, "a": 2 })), json!({ "a": 2, "b": 1 }));
80/// ```
81#[must_use]
82pub fn sort_keys(value: &Value) -> Value {
83    sort_keys_with(value, &Options::new())
84}
85
86/// Sort the keys of `value` with the given [`Options`], using the default UTF-16 ordering.
87#[must_use]
88pub fn sort_keys_with(value: &Value, options: &Options) -> Value {
89    sort_keys_by(value, options, compare_utf16)
90}
91
92/// Sort the keys of `value` with the given [`Options`] and a custom comparator.
93///
94/// ```
95/// # use serde_json::json;
96/// # use sort_keys::{sort_keys_by, Options};
97/// // Reverse order:
98/// let sorted = sort_keys_by(&json!({ "a": 1, "b": 2 }), &Options::new(), |a, b| b.cmp(a));
99/// assert_eq!(sorted, json!({ "b": 2, "a": 1 }));
100/// ```
101#[must_use]
102pub fn sort_keys_by<F>(value: &Value, options: &Options, compare: F) -> Value
103where
104    F: Fn(&str, &str) -> Ordering,
105{
106    sort_value(value, options, &compare)
107}
108
109fn sort_value<F>(value: &Value, options: &Options, compare: &F) -> Value
110where
111    F: Fn(&str, &str) -> Ordering,
112{
113    match value {
114        Value::Object(map) => Value::Object(sort_object(map, options, compare)),
115        Value::Array(array) => Value::Array(sort_array(array, options, compare)),
116        other => other.clone(),
117    }
118}
119
120fn sort_object<F>(map: &Map<String, Value>, options: &Options, compare: &F) -> Map<String, Value>
121where
122    F: Fn(&str, &str) -> Ordering,
123{
124    let mut ignored: Vec<&String> = Vec::new();
125    let mut to_sort: Vec<&String> = Vec::new();
126    for key in map.keys() {
127        if options
128            .ignore_keys
129            .iter()
130            .any(|ignored_key| ignored_key == key)
131        {
132            ignored.push(key);
133        } else {
134            to_sort.push(key);
135        }
136    }
137    to_sort.sort_by(|a, b| compare(a, b));
138
139    let mut result = Map::new();
140    for key in ignored.into_iter().chain(to_sort) {
141        let value = &map[key];
142        let new_value = if options.deep {
143            recurse_value(value, options, compare)
144        } else {
145            value.clone()
146        };
147        result.insert(key.clone(), new_value);
148    }
149    result
150}
151
152fn sort_array<F>(array: &[Value], options: &Options, compare: &F) -> Vec<Value>
153where
154    F: Fn(&str, &str) -> Ordering,
155{
156    array
157        .iter()
158        .map(|item| {
159            if options.deep {
160                recurse_value(item, options, compare)
161            } else {
162                item.clone()
163            }
164        })
165        .collect()
166}
167
168/// Recurse into a value if it is a container (objects are key-sorted, arrays have their items
169/// processed); other values are cloned.
170fn recurse_value<F>(value: &Value, options: &Options, compare: &F) -> Value
171where
172    F: Fn(&str, &str) -> Ordering,
173{
174    match value {
175        Value::Object(map) => Value::Object(sort_object(map, options, compare)),
176        Value::Array(array) => Value::Array(sort_array(array, options, compare)),
177        other => other.clone(),
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use serde_json::json;
185
186    #[test]
187    fn shallow() {
188        assert_eq!(
189            sort_keys(&json!({ "c": 0, "a": 0, "b": 0 })),
190            json!({ "a": 0, "b": 0, "c": 0 })
191        );
192        // Default shallow: nested keys not sorted.
193        assert_eq!(
194            sort_keys(&json!({ "b": { "d": 0, "c": 0 }, "a": 0 })),
195            json!({ "a": 0, "b": { "d": 0, "c": 0 } })
196        );
197    }
198
199    #[test]
200    fn deep() {
201        assert_eq!(
202            sort_keys_with(
203                &json!({ "b": { "d": 0, "c": 0 }, "a": 0 }),
204                &Options::new().deep(true)
205            ),
206            json!({ "a": 0, "b": { "c": 0, "d": 0 } })
207        );
208        assert_eq!(
209            sort_keys_with(
210                &json!({ "b": [{ "d": 0, "c": 0 }], "a": 0 }),
211                &Options::new().deep(true)
212            ),
213            json!({ "a": 0, "b": [{ "c": 0, "d": 0 }] })
214        );
215        // Top-level array: object items sorted only when deep.
216        assert_eq!(
217            sort_keys_with(&json!([{ "c": 0, "a": 0 }]), &Options::new().deep(true)),
218            json!([{ "a": 0, "c": 0 }])
219        );
220        assert_eq!(
221            sort_keys(&json!([{ "c": 0, "a": 0 }])),
222            json!([{ "c": 0, "a": 0 }])
223        );
224    }
225
226    #[test]
227    fn custom_compare() {
228        let reversed = sort_keys_by(
229            &json!({ "a": 0, "c": 0, "b": 0 }),
230            &Options::new(),
231            |a, b| b.cmp(a),
232        );
233        assert_eq!(reversed, json!({ "c": 0, "b": 0, "a": 0 }));
234    }
235
236    #[test]
237    fn ignore_keys() {
238        // Ignored keys keep their original order and come first.
239        let result = sort_keys_with(
240            &json!({ "z": 0, "b": 0, "a": 0 }),
241            &Options::new().ignore_keys(["z"]),
242        );
243        assert_eq!(result, json!({ "z": 0, "a": 0, "b": 0 }));
244    }
245
246    #[test]
247    fn scalars_passthrough() {
248        assert_eq!(sort_keys(&json!(42)), json!(42));
249        assert_eq!(sort_keys(&json!("s")), json!("s"));
250    }
251}