Skip to content

Commit dae5993

Browse files
committed
Document hardened JSON strategy contracts
1 parent 2e7ca4b commit dae5993

1 file changed

Lines changed: 99 additions & 108 deletions

File tree

docs/json_collection_strategies.rst

Lines changed: 99 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@ JSON Collection Strategies
22
==========================
33

44
``DeepJSONDiff`` is an opt-in facade for comparing JSON-like values whose nested
5-
arrays require path-specific semantics. It canonicalizes both inputs without
6-
mutating them and delegates the final recursive comparison to ``DeepDiff``.
7-
Existing ``DeepDiff`` behaviour is unchanged.
5+
arrays require path-specific semantics. It builds canonical caller-isolated
6+
views and delegates the final recursive comparison to ``DeepDiff``. Existing
7+
``DeepDiff`` behaviour is unchanged.
88

99
Basic identity matching
1010
-----------------------
1111

12-
Use ``match_by`` when a list contains records with a stable business identity.
13-
Reordering does not create differences, while additions, removals and field
14-
changes remain visible::
12+
Use ``match_by`` when list records have a stable business identity::
1513

1614
from deepdiff import CollectionStrategy, DeepJSONDiff
1715

@@ -26,144 +24,137 @@ changes remain visible::
2624
],
2725
)
2826

29-
Nested arrays and composite keys
30-
--------------------------------
27+
Reordering does not create differences. Additions, removals, and field changes
28+
remain visible against canonical identity-keyed paths.
3129

32-
Array wildcards select exactly one array index under every matching parent.
33-
Object-key wildcards likewise select exactly one key. Relative identity fields
34-
may be nested and composite::
35-
36-
CollectionStrategy(
37-
path="$.orders[*].items",
38-
match_by=("product.id", "warehouse.code"),
39-
)
30+
Selectors
31+
---------
4032

41-
CollectionStrategy(
42-
path="$.*.users",
43-
match_by=("id",),
44-
)
33+
Selectors use a deliberately small JSONPath-like grammar:
4534

46-
Identity fields must resolve to JSON scalar values: ``None``, booleans,
47-
integers, finite floats, or strings. Structured values such as dictionaries and
48-
lists are rejected because they do not provide a stable business identity.
35+
- ``$.users`` selects an object key.
36+
- ``$.groups[0].users`` selects one array index.
37+
- ``$.groups[*].users`` matches exactly one array index.
38+
- ``$.*.users`` matches exactly one object key.
39+
- ``$['a-b']`` selects a quoted key that cannot be written safely in dot form.
4940

50-
Filtering
51-
---------
41+
Wildcards never cross additional levels. Diagnostic paths use the same quoted
42+
key syntax and can be reused as selectors.
5243

53-
``filter_func`` is applied symmetrically to defensive copies of both inputs
54-
before matching and comparison. A filter may therefore inspect or mutate the
55-
value it receives without changing caller-owned payloads. Filter counts are
56-
available through ``get_stats()``::
44+
Relative identity and sort fields use dotted extraction and may include numeric
45+
list indexes, such as ``product.id`` or ``versions.0.number``.
5746

58-
strategy = CollectionStrategy(
59-
path="$.users",
60-
match_by=("id",),
61-
filter_func=lambda item: item.get("active") is True,
62-
)
47+
Identity values
48+
---------------
6349

64-
Sorting and order-insensitive arrays
65-
------------------------------------
50+
Identity fields must resolve, after normalization, to finite JSON scalar values:
51+
``None``, booleans, integers, finite floats, or strings. Structured and
52+
non-finite values are rejected.
6653

67-
``sort_by`` creates deterministic ordering for arrays where identity matching is
68-
not required. Numeric values use numeric ordering; missing, null, mixed, and
69-
structured values use a stable type-aware order.
54+
Composite identities are encoded using a canonical type-preserving format, so
55+
values such as integer ``1``, float ``1.0``, and string ``"1"`` remain distinct
56+
and delimiter characters cannot cause collisions.
7057

71-
``compare_as_set`` performs multiset (bag) comparison for scalar arrays. Order
72-
is ignored but duplicate counts remain significant::
58+
Filtering, normalization, and exclusion
59+
---------------------------------------
7360

74-
CollectionStrategy(path="$.events", sort_by=("timestamp", "id"))
75-
CollectionStrategy(path="$.roles", compare_as_set=True)
61+
``filter_func`` and normalizers receive defensive copies and may mutate them
62+
without modifying caller-owned inputs. Copies are created only when callbacks
63+
are configured; otherwise canonicalization rebuilds the structure directly.
7664

77-
``compare_as_set`` cannot be combined with ``match_by`` or ``sort_by``.
65+
Processing order is:
7866

79-
Missing identities
80-
------------------
67+
1. Copy when callbacks require isolation.
68+
2. Apply ``filter_func``.
69+
3. Apply normalizers.
70+
4. Extract identity fields.
71+
5. Remove ``exclude_fields``.
72+
6. Canonicalize nested content.
8173

82-
The default ``MissingIdentityPolicy.FALLBACK`` retains records lacking one or
83-
more identity fields and compares them using their relative order. Strict and
84-
exclusion policies are also available. Enum members and their string values are
85-
accepted::
74+
Identity fields may therefore also appear in ``exclude_fields``. They are used
75+
for matching but need not remain in the compared record.
8676

87-
from deepdiff import MissingIdentityPolicy
77+
Sorting
78+
-------
8879

89-
CollectionStrategy(
90-
path="$.items",
91-
match_by=("id",),
92-
missing_identity=MissingIdentityPolicy.ERROR,
93-
)
80+
``sort_by`` creates a total, type-stable order for JSON-compatible values.
81+
Integers and floats are intentionally distinguished. Finite values, infinities,
82+
and NaN values have deterministic positions and do not produce mixed-type sort
83+
errors.
9484

95-
CollectionStrategy(
96-
path="$.items",
97-
match_by=("id",),
98-
missing_identity="exclude",
99-
)
85+
Structured sort keys are supported only for JSON lists and mappings with string
86+
keys. Tuples, sets, mappings with non-string keys, and arbitrary objects are
87+
rejected rather than ordered through unstable ``repr`` output.
10088

101-
Duplicate identities
102-
--------------------
89+
Order-insensitive scalar arrays
90+
-------------------------------
10391

104-
Duplicate identities raise ``DuplicateIdentityError`` by default. To compare a
105-
duplicate group, use ``DuplicateIdentityPolicy.GROUP`` and preferably provide a
106-
secondary ``sort_by`` key::
92+
``compare_as_set=True`` performs multiset (bag) comparison:
10793

108-
from deepdiff import DuplicateIdentityPolicy
94+
- order is ignored;
95+
- duplicate counts remain significant;
96+
- integer and float values remain type-distinct;
97+
- only finite JSON scalar values are accepted.
10998

110-
CollectionStrategy(
111-
path="$.items",
112-
match_by=("id",),
113-
sort_by=("version",),
114-
duplicates=DuplicateIdentityPolicy.GROUP,
115-
)
99+
It cannot be combined with ``match_by`` or ``sort_by``.
116100

117-
Duplicate identities are encoded with a canonical, type-preserving format, so
118-
distinct composite identities cannot collide.
101+
Missing identities
102+
------------------
119103

120-
Normalization and volatile fields
121-
---------------------------------
104+
The default ``MissingIdentityPolicy.FALLBACK`` retains records missing one or
105+
more identity fields and compares them in relative order. ``EXCLUDE`` omits
106+
them, and ``ERROR`` raises ``IdentityExtractionError``. Enum members and their
107+
string values are accepted.
122108

123-
Normalizers run before identity extraction and comparison. Each selected item
124-
is deep-copied before filters and normalizers run, so callbacks may mutate their
125-
argument without mutating the caller's input. ``exclude_fields`` removes
126-
volatile top-level fields from each selected array item::
109+
Duplicate identities
110+
--------------------
127111

128-
CollectionStrategy(
129-
path="$.events",
130-
match_by=("eventId",),
131-
normalizers=(normalize_region,),
132-
exclude_fields=("requestId", "generatedAt"),
133-
)
112+
Duplicate identities raise ``DuplicateIdentityError`` by default.
113+
``DuplicateIdentityPolicy.GROUP`` retains every record under the identity.
114+
Provide ``sort_by`` when duplicate-group order is not meaningful.
134115

135116
Rule precedence
136117
---------------
137118

138-
When multiple rules match a concrete path, higher ``priority`` wins. At equal
139-
priority, the pattern with more exact tokens wins. Equally specific matches are
140-
rejected as ambiguous instead of being resolved silently.
119+
Higher ``priority`` wins when multiple strategies match. At equal priority, the
120+
pattern with more exact tokens wins. Equally specific matches are rejected as
121+
ambiguous.
141122

142123
Diagnostics
143124
-----------
144125

145-
``get_stats()`` returns per-concrete-path execution information, including
146-
input counts, filtered counts, missing identities, duplicate groups, and the
147-
selected strategy name.
126+
``get_stats()`` separates diagnostics by input side::
148127

149-
Result interface
150-
----------------
128+
stats = diff.get_stats()
129+
left_users = stats["left"]["$.users"]
130+
right_users = stats["right"]["$.users"]
151131

152-
``DeepJSONDiff`` implements the standard read-only mapping interface and
153-
delegates serialization to the underlying ``DeepDiff`` result::
132+
Each entry includes the selected strategy, input item count, filtered count,
133+
missing-identity count, and duplicate-group count. Keeping sides separate
134+
prevents statistics for different logical parents from being merged when a
135+
parent identity-matched collection is reordered.
154136

155-
if diff:
156-
print(diff["values_changed"])
137+
DeepDiff keyword compatibility
138+
------------------------------
157139

158-
for category, changes in diff.items():
159-
print(category, changes)
140+
Identity matching changes selected arrays into canonical mappings. DeepDiff
141+
options that interpret caller-visible paths or iterable positions would
142+
therefore operate on a different structure. ``DeepJSONDiff`` rejects these
143+
path-sensitive options instead of silently changing their meaning:
160144

161-
print(diff.to_json(sort_keys=True))
145+
- ``include_paths``
146+
- ``exclude_paths``
147+
- ``exclude_regex_paths``
148+
- ``ignore_order_func``
149+
- ``iterable_compare_func``
150+
- ``custom_operators``
162151

163-
Compatibility
164-
-------------
152+
Other keyword arguments are forwarded to the underlying ``DeepDiff`` instance.
153+
154+
Result interface
155+
----------------
165156

166-
``DeepJSONDiff`` is separate from ``DeepDiff`` by design. Existing constructor
167-
parameters, result views, Delta behaviour, iterable callbacks, and
168-
multiprocessing paths are not modified. Keyword arguments unrelated to
169-
collection strategies are forwarded to the underlying ``DeepDiff`` instance.
157+
``DeepJSONDiff`` is a composition-based facade, not a complete ``DeepDiff``
158+
subclass. It implements the read-only mapping interface and exposes ``diff`` for
159+
direct access to the underlying result. ``to_dict()`` and ``to_json()`` are
160+
delegated explicitly.

0 commit comments

Comments
 (0)