assert_json_diff/
lib.rs

1//! This crate includes macros for comparing two serializable values by diffing their JSON
2//! representations. It is designed to give much more helpful error messages than the standard
3//! [`assert_eq!`]. It basically does a diff of the two objects and tells you the exact
4//! differences. This is useful when asserting that two large JSON objects are the same.
5//!
6//! It uses the [serde] and [serde_json] to perform the serialization.
7//!
8//! [serde]: https://crates.io/crates/serde
9//! [serde_json]: https://crates.io/crates/serde_json
10//! [`assert_eq!`]: https://doc.rust-lang.org/std/macro.assert_eq.html
11//!
12//! ## Partial matching
13//!
14//! If you want to assert that one JSON value is "included" in another use
15//! [`assert_json_include`](macro.assert_json_include.html):
16//!
17//! ```should_panic
18//! use assert_json_diff::assert_json_include;
19//! use serde_json::json;
20//!
21//! let a = json!({
22//!     "data": {
23//!         "users": [
24//!             {
25//!                 "id": 1,
26//!                 "country": {
27//!                     "name": "Denmark"
28//!                 }
29//!             },
30//!             {
31//!                 "id": 24,
32//!                 "country": {
33//!                     "name": "Denmark"
34//!                 }
35//!             }
36//!         ]
37//!     }
38//! });
39//!
40//! let b = json!({
41//!     "data": {
42//!         "users": [
43//!             {
44//!                 "id": 1,
45//!                 "country": {
46//!                     "name": "Sweden"
47//!                 }
48//!             },
49//!             {
50//!                 "id": 2,
51//!                 "country": {
52//!                     "name": "Denmark"
53//!                 }
54//!             }
55//!         ]
56//!     }
57//! });
58//!
59//! assert_json_include!(actual: a, expected: b)
60//! ```
61//!
62//! This will panic with the error message:
63//!
64//! ```text
65//! json atoms at path ".data.users[0].country.name" are not equal:
66//!     expected:
67//!         "Sweden"
68//!     actual:
69//!         "Denmark"
70//!
71//! json atoms at path ".data.users[1].id" are not equal:
72//!     expected:
73//!         2
74//!     actual:
75//!         24
76//! ```
77//!
78//! [`assert_json_include`](macro.assert_json_include.html) allows extra data in `actual` but not in `expected`. That is so you can verify just a part
79//! of the JSON without having to specify the whole thing. For example this test passes:
80//!
81//! ```
82//! use assert_json_diff::assert_json_include;
83//! use serde_json::json;
84//!
85//! assert_json_include!(
86//!     actual: json!({
87//!         "a": { "b": 1 },
88//!     }),
89//!     expected: json!({
90//!         "a": {},
91//!     })
92//! )
93//! ```
94//!
95//! However `expected` cannot contain additional data so this test fails:
96//!
97//! ```should_panic
98//! use assert_json_diff::assert_json_include;
99//! use serde_json::json;
100//!
101//! assert_json_include!(
102//!     actual: json!({
103//!         "a": {},
104//!     }),
105//!     expected: json!({
106//!         "a": { "b": 1 },
107//!     })
108//! )
109//! ```
110//!
111//! That will print
112//!
113//! ```text
114//! json atom at path ".a.b" is missing from actual
115//! ```
116//!
117//! ## Exact matching
118//!
119//! If you want to ensure two JSON values are *exactly* the same, use [`assert_json_eq`](macro.assert_json_eq.html).
120//!
121//! ```rust,should_panic
122//! use assert_json_diff::assert_json_eq;
123//! use serde_json::json;
124//!
125//! assert_json_eq!(
126//!     json!({ "a": { "b": 1 } }),
127//!     json!({ "a": {} })
128//! )
129//! ```
130//!
131//! This will panic with the error message:
132//!
133//! ```text
134//! json atom at path ".a.b" is missing from lhs
135//! ```
136//!
137//! ## Further customization
138//!
139//! You can use [`assert_json_matches`] to further customize the comparison.
140
141#![deny(
142    missing_docs,
143    unused_imports,
144    missing_debug_implementations,
145    missing_copy_implementations,
146    trivial_casts,
147    trivial_numeric_casts,
148    unsafe_code,
149    unstable_features,
150    unused_import_braces,
151    unused_qualifications,
152    unknown_lints
153)]
154
155use diff::diff;
156use serde::Serialize;
157
158mod core_ext;
159mod diff;
160
161/// Compare two JSON values for an inclusive match.
162///
163/// It allows `actual` to contain additional data. If you want an exact match use
164/// [`assert_json_eq`](macro.assert_json_eq.html) instead.
165///
166/// See [crate documentation](index.html) for examples.
167#[macro_export]
168macro_rules! assert_json_include {
169    (actual: $actual:expr, expected: $expected:expr $(,)?) => {{
170        $crate::assert_json_matches!(
171            $actual,
172            $expected,
173            $crate::Config::new($crate::CompareMode::Inclusive)
174        )
175    }};
176    (expected: $expected:expr, actual: $actual:expr $(,)?) => {{
177        $crate::assert_json_include!(actual: $actual, expected: $expected)
178    }};
179}
180
181/// Compare two JSON values for an exact match.
182///
183/// If you want an inclusive match use [`assert_json_include`](macro.assert_json_include.html) instead.
184///
185/// See [crate documentation](index.html) for examples.
186#[macro_export]
187macro_rules! assert_json_eq {
188    ($lhs:expr, $rhs:expr $(,)?) => {{
189        $crate::assert_json_matches!($lhs, $rhs, $crate::Config::new($crate::CompareMode::Strict))
190    }};
191}
192
193/// Compare two JSON values according to a configuration.
194///
195/// ```
196/// use assert_json_diff::{
197///     CompareMode,
198///     Config,
199///     NumericMode,
200///     assert_json_matches,
201/// };
202/// use serde_json::json;
203///
204/// let config = Config::new(CompareMode::Strict).numeric_mode(NumericMode::AssumeFloat);
205///
206/// assert_json_matches!(
207///     json!({
208///         "a": { "b": [1, 2, 3.0] },
209///     }),
210///     json!({
211///         "a": { "b": [1, 2.0, 3] },
212///     }),
213///     config,
214/// )
215/// ```
216///
217/// When using `CompareMode::Inclusive` the first argument is `actual` and the second argument is
218/// `expected`. Example:
219///
220/// ```
221/// # use assert_json_diff::{
222/// #     CompareMode,
223/// #     Config,
224/// #     NumericMode,
225/// #     assert_json_matches,
226/// #     assert_json_include,
227/// # };
228/// # use serde_json::json;
229/// #
230/// // This
231/// assert_json_matches!(
232///     json!({
233///         "a": { "b": 1 },
234///     }),
235///     json!({
236///         "a": {},
237///     }),
238///     Config::new(CompareMode::Inclusive),
239/// );
240///
241/// // Is the same as this
242/// assert_json_include!(
243///     actual: json!({
244///         "a": { "b": 1 },
245///     }),
246///     expected: json!({
247///         "a": {},
248///     }),
249/// );
250/// ```
251#[macro_export]
252macro_rules! assert_json_matches {
253    ($lhs:expr, $rhs:expr, $config:expr $(,)?) => {{
254        if let Err(error) = $crate::assert_json_matches_no_panic(&$lhs, &$rhs, $config) {
255            panic!("\n\n{}\n\n", error);
256        }
257    }};
258}
259
260/// Compares two JSON values without panicking.
261///
262/// Instead it returns a `Result` where the error is the message that would be passed to `panic!`.
263/// This is might be useful if you want to control how failures are reported and don't want to deal
264/// with panics.
265pub fn assert_json_matches_no_panic<Lhs, Rhs>(
266    lhs: &Lhs,
267    rhs: &Rhs,
268    config: Config,
269) -> Result<(), String>
270where
271    Lhs: Serialize,
272    Rhs: Serialize,
273{
274    let lhs = serde_json::to_value(lhs).unwrap_or_else(|err| {
275        panic!(
276            "Couldn't convert left hand side value to JSON. Serde error: {}",
277            err
278        )
279    });
280    let rhs = serde_json::to_value(rhs).unwrap_or_else(|err| {
281        panic!(
282            "Couldn't convert right hand side value to JSON. Serde error: {}",
283            err
284        )
285    });
286
287    let diffs = diff(&lhs, &rhs, config);
288
289    if diffs.is_empty() {
290        Ok(())
291    } else {
292        let msg = diffs
293            .into_iter()
294            .map(|d| d.to_string())
295            .collect::<Vec<_>>()
296            .join("\n\n");
297        Err(msg)
298    }
299}
300
301/// Configuration for how JSON values should be compared.
302#[derive(Debug, Clone, PartialEq, Eq)]
303#[allow(missing_copy_implementations)]
304pub struct Config {
305    pub(crate) compare_mode: CompareMode,
306    pub(crate) numeric_mode: NumericMode,
307}
308
309impl Config {
310    /// Create a new [`Config`] using the given [`CompareMode`].
311    ///
312    /// The default `numeric_mode` is be [`NumericMode::Strict`].
313    pub fn new(compare_mode: CompareMode) -> Self {
314        Self {
315            compare_mode,
316            numeric_mode: NumericMode::Strict,
317        }
318    }
319
320    /// Change the config's numeric mode.
321    ///
322    /// The default `numeric_mode` is be [`NumericMode::Strict`].
323    pub fn numeric_mode(mut self, numeric_mode: NumericMode) -> Self {
324        self.numeric_mode = numeric_mode;
325        self
326    }
327
328    /// Change the config's compare mode.
329    pub fn compare_mode(mut self, compare_mode: CompareMode) -> Self {
330        self.compare_mode = compare_mode;
331        self
332    }
333}
334
335/// Mode for how JSON values should be compared.
336#[derive(Debug, Copy, Clone, PartialEq, Eq)]
337pub enum CompareMode {
338    /// The two JSON values don't have to be exactly equal. The "actual" value is only required to
339    /// be "contained" inside "expected". See [crate documentation](index.html) for examples.
340    ///
341    /// The mode used with [`assert_json_include`].
342    Inclusive,
343    /// The two JSON values must be exactly equal.
344    ///
345    /// The mode used with [`assert_json_eq`].
346    Strict,
347}
348
349/// How should numbers be compared.
350#[derive(Debug, Copy, Clone, PartialEq, Eq)]
351pub enum NumericMode {
352    /// Different numeric types aren't considered equal.
353    Strict,
354    /// All numeric types are converted to float before comparison.
355    AssumeFloat,
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361    use serde_json::{json, Value};
362    use std::fmt::Write;
363
364    #[test]
365    fn boolean_root() {
366        let result = test_partial_match(json!(true), json!(true));
367        assert_output_eq(result, Ok(()));
368
369        let result = test_partial_match(json!(false), json!(false));
370        assert_output_eq(result, Ok(()));
371
372        let result = test_partial_match(json!(false), json!(true));
373        assert_output_eq(
374            result,
375            Err(r#"json atoms at path "(root)" are not equal:
376    expected:
377        true
378    actual:
379        false"#),
380        );
381
382        let result = test_partial_match(json!(true), json!(false));
383        assert_output_eq(
384            result,
385            Err(r#"json atoms at path "(root)" are not equal:
386    expected:
387        false
388    actual:
389        true"#),
390        );
391    }
392
393    #[test]
394    fn string_root() {
395        let result = test_partial_match(json!("true"), json!("true"));
396        assert_output_eq(result, Ok(()));
397
398        let result = test_partial_match(json!("false"), json!("false"));
399        assert_output_eq(result, Ok(()));
400
401        let result = test_partial_match(json!("false"), json!("true"));
402        assert_output_eq(
403            result,
404            Err(r#"json atoms at path "(root)" are not equal:
405    expected:
406        "true"
407    actual:
408        "false""#),
409        );
410
411        let result = test_partial_match(json!("true"), json!("false"));
412        assert_output_eq(
413            result,
414            Err(r#"json atoms at path "(root)" are not equal:
415    expected:
416        "false"
417    actual:
418        "true""#),
419        );
420    }
421
422    #[test]
423    fn number_root() {
424        let result = test_partial_match(json!(1), json!(1));
425        assert_output_eq(result, Ok(()));
426
427        let result = test_partial_match(json!(0), json!(0));
428        assert_output_eq(result, Ok(()));
429
430        let result = test_partial_match(json!(0), json!(1));
431        assert_output_eq(
432            result,
433            Err(r#"json atoms at path "(root)" are not equal:
434    expected:
435        1
436    actual:
437        0"#),
438        );
439
440        let result = test_partial_match(json!(1), json!(0));
441        assert_output_eq(
442            result,
443            Err(r#"json atoms at path "(root)" are not equal:
444    expected:
445        0
446    actual:
447        1"#),
448        );
449    }
450
451    #[test]
452    fn null_root() {
453        let result = test_partial_match(json!(null), json!(null));
454        assert_output_eq(result, Ok(()));
455
456        let result = test_partial_match(json!(null), json!(1));
457        assert_output_eq(
458            result,
459            Err(r#"json atoms at path "(root)" are not equal:
460    expected:
461        1
462    actual:
463        null"#),
464        );
465
466        let result = test_partial_match(json!(1), json!(null));
467        assert_output_eq(
468            result,
469            Err(r#"json atoms at path "(root)" are not equal:
470    expected:
471        null
472    actual:
473        1"#),
474        );
475    }
476
477    #[test]
478    fn into_object() {
479        let result = test_partial_match(json!({ "a": true }), json!({ "a": true }));
480        assert_output_eq(result, Ok(()));
481
482        let result = test_partial_match(json!({ "a": false }), json!({ "a": true }));
483        assert_output_eq(
484            result,
485            Err(r#"json atoms at path ".a" are not equal:
486    expected:
487        true
488    actual:
489        false"#),
490        );
491
492        let result =
493            test_partial_match(json!({ "a": { "b": true } }), json!({ "a": { "b": true } }));
494        assert_output_eq(result, Ok(()));
495
496        let result = test_partial_match(json!({ "a": true }), json!({ "a": { "b": true } }));
497        assert_output_eq(
498            result,
499            Err(r#"json atoms at path ".a" are not equal:
500    expected:
501        {
502          "b": true
503        }
504    actual:
505        true"#),
506        );
507
508        let result = test_partial_match(json!({}), json!({ "a": true }));
509        assert_output_eq(
510            result,
511            Err(r#"json atom at path ".a" is missing from actual"#),
512        );
513
514        let result = test_partial_match(json!({ "a": { "b": true } }), json!({ "a": true }));
515        assert_output_eq(
516            result,
517            Err(r#"json atoms at path ".a" are not equal:
518    expected:
519        true
520    actual:
521        {
522          "b": true
523        }"#),
524        );
525    }
526
527    #[test]
528    fn into_array() {
529        let result = test_partial_match(json!([1]), json!([1]));
530        assert_output_eq(result, Ok(()));
531
532        let result = test_partial_match(json!([2]), json!([1]));
533        assert_output_eq(
534            result,
535            Err(r#"json atoms at path "[0]" are not equal:
536    expected:
537        1
538    actual:
539        2"#),
540        );
541
542        let result = test_partial_match(json!([1, 2, 4]), json!([1, 2, 3]));
543        assert_output_eq(
544            result,
545            Err(r#"json atoms at path "[2]" are not equal:
546    expected:
547        3
548    actual:
549        4"#),
550        );
551
552        let result = test_partial_match(json!({ "a": [1, 2, 3]}), json!({ "a": [1, 2, 4]}));
553        assert_output_eq(
554            result,
555            Err(r#"json atoms at path ".a[2]" are not equal:
556    expected:
557        4
558    actual:
559        3"#),
560        );
561
562        let result = test_partial_match(json!({ "a": [1, 2, 3]}), json!({ "a": [1, 2]}));
563        assert_output_eq(result, Ok(()));
564
565        let result = test_partial_match(json!({ "a": [1, 2]}), json!({ "a": [1, 2, 3]}));
566        assert_output_eq(
567            result,
568            Err(r#"json atom at path ".a[2]" is missing from actual"#),
569        );
570    }
571
572    #[test]
573    fn exact_matching() {
574        let result = test_exact_match(json!(true), json!(true));
575        assert_output_eq(result, Ok(()));
576
577        let result = test_exact_match(json!("s"), json!("s"));
578        assert_output_eq(result, Ok(()));
579
580        let result = test_exact_match(json!("a"), json!("b"));
581        assert_output_eq(
582            result,
583            Err(r#"json atoms at path "(root)" are not equal:
584    lhs:
585        "a"
586    rhs:
587        "b""#),
588        );
589
590        let result = test_exact_match(
591            json!({ "a": [1, { "b": 2 }] }),
592            json!({ "a": [1, { "b": 3 }] }),
593        );
594        assert_output_eq(
595            result,
596            Err(r#"json atoms at path ".a[1].b" are not equal:
597    lhs:
598        2
599    rhs:
600        3"#),
601        );
602    }
603
604    #[test]
605    fn exact_match_output_message() {
606        let result = test_exact_match(json!({ "a": { "b": 1 } }), json!({ "a": {} }));
607        assert_output_eq(
608            result,
609            Err(r#"json atom at path ".a.b" is missing from rhs"#),
610        );
611
612        let result = test_exact_match(json!({ "a": {} }), json!({ "a": { "b": 1 } }));
613        assert_output_eq(
614            result,
615            Err(r#"json atom at path ".a.b" is missing from lhs"#),
616        );
617    }
618
619    fn assert_output_eq(actual: Result<(), String>, expected: Result<(), &str>) {
620        match (actual, expected) {
621            (Ok(()), Ok(())) => {}
622
623            (Err(actual_error), Ok(())) => {
624                let mut f = String::new();
625                writeln!(f, "Did not expect error, but got").unwrap();
626                writeln!(f, "{}", actual_error).unwrap();
627                panic!("{}", f);
628            }
629
630            (Ok(()), Err(expected_error)) => {
631                let expected_error = expected_error.to_string();
632                let mut f = String::new();
633                writeln!(f, "Expected error, but did not get one. Expected error:").unwrap();
634                writeln!(f, "{}", expected_error).unwrap();
635                panic!("{}", f);
636            }
637
638            (Err(actual_error), Err(expected_error)) => {
639                let expected_error = expected_error.to_string();
640                if actual_error != expected_error {
641                    let mut f = String::new();
642                    writeln!(f, "Errors didn't match").unwrap();
643                    writeln!(f, "Expected:").unwrap();
644                    writeln!(f, "{}", expected_error).unwrap();
645                    writeln!(f, "Got:").unwrap();
646                    writeln!(f, "{}", actual_error).unwrap();
647                    panic!("{}", f);
648                }
649            }
650        }
651    }
652
653    fn test_partial_match(lhs: Value, rhs: Value) -> Result<(), String> {
654        assert_json_matches_no_panic(&lhs, &rhs, Config::new(CompareMode::Inclusive))
655    }
656
657    fn test_exact_match(lhs: Value, rhs: Value) -> Result<(), String> {
658        assert_json_matches_no_panic(&lhs, &rhs, Config::new(CompareMode::Strict))
659    }
660}