<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Timur</title>
    <description>The latest articles on DEV Community by Timur (@saint_moore).</description>
    <link>https://dev.to/saint_moore</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4225355%2F96e7690f-d775-4575-a50c-c212e0bd8d3a.jpg</url>
      <title>DEV Community: Timur</title>
      <link>https://dev.to/saint_moore</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saint_moore"/>
    <language>en</language>
    <item>
      <title>Comparing JSON Files: Practical Diffing Strategies for Developers</title>
      <dc:creator>Timur</dc:creator>
      <pubDate>Wed, 04 Nov 2026 04:44:53 +0000</pubDate>
      <link>https://dev.to/saint_moore/comparing-json-files-practical-diffing-strategies-for-developers-2ndb</link>
      <guid>https://dev.to/saint_moore/comparing-json-files-practical-diffing-strategies-for-developers-2ndb</guid>
      <description>&lt;h1&gt;
  
  
  A story you have probably lived
&lt;/h1&gt;

&lt;p&gt;Staging deploy goes out. Something breaks downstream.&lt;/p&gt;

&lt;p&gt;So you do the obvious thing: export the payload from production, export the payload from staging, and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diff &lt;span class="nt"&gt;-u&lt;/span&gt; prod.json staging.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And diff hands you 400 changed lines. Two files. Same data. Completely unchanged schema.&lt;/p&gt;

&lt;p&gt;Except one service decided to sort keys alphabetically, and the other preserved insertion order. And one of them pretty-printed the output while the other minified it. Both sets of choices are perfectly valid JSON. &lt;code&gt;diff&lt;/code&gt; sees the first file and the second file as having almost nothing in common.&lt;/p&gt;

&lt;p&gt;That is the moment it is worth understanding what is actually going on, because the fix is not "use a diff tool with nicer colors."&lt;/p&gt;

&lt;p&gt;Let me talk about why this happens, then the three approaches I use depending on where I am standing.&lt;/p&gt;

&lt;h1&gt;
  
  
  Why line-based diffing breaks on JSON
&lt;/h1&gt;

&lt;p&gt;Two reasons, and both come from JSON's own spec.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Object keys have no order.&lt;/strong&gt; RFC 8259 describes JSON as an unordered collection of key/value pairs. The spec is explicit: key order carries no semantic meaning. So &lt;code&gt;{"id":1042,"status":"active"}&lt;/code&gt; and &lt;code&gt;{"status":"active","id":1042}&lt;/code&gt; are the &lt;em&gt;same document&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;POSIX &lt;code&gt;diff&lt;/code&gt; has no idea about that. It computes the longest common subsequence of lines, and a line sequence is absolute truth to it. Reorder the keys and you get a full-file churn report even though not a single value changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whitespace is arbitrary.&lt;/strong&gt; JSON permits any amount of whitespace around structural tokens: space, tab, &lt;code&gt;\n&lt;/code&gt;, &lt;code&gt;\r&lt;/code&gt;. A minified payload and its pretty-printed twin tokenize to identical trees. To &lt;code&gt;diff&lt;/code&gt;, they have zero matching lines.&lt;/p&gt;

&lt;p&gt;Here are the two payloads from the story above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;payload_a.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1042&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"prod"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;payload_b.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;same&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;record,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;reordered&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"prod"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1042&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"active"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;diff &lt;span class="nt"&gt;-u&lt;/span&gt; payload_a.json payload_b.json
@@ &lt;span class="nt"&gt;-1&lt;/span&gt;,5 +1,5 @@
 &lt;span class="o"&gt;{&lt;/span&gt;
+  &lt;span class="s2"&gt;"tags"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"prod"&lt;/span&gt;, &lt;span class="s2"&gt;"auth"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;,
   &lt;span class="s2"&gt;"id"&lt;/span&gt;: 1042,
-  &lt;span class="s2"&gt;"status"&lt;/span&gt;: &lt;span class="s2"&gt;"active"&lt;/span&gt;,
-  &lt;span class="s2"&gt;"tags"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"prod"&lt;/span&gt;, &lt;span class="s2"&gt;"auth"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;,
+  &lt;span class="s2"&gt;"status"&lt;/span&gt;: &lt;span class="s2"&gt;"active"&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four "changed" lines. Zero actual differences. In a deployment audit this is noise, and in an automated change-detection script it is a broken build.&lt;/p&gt;

&lt;p&gt;The underlying fix is to compare &lt;em&gt;structure&lt;/em&gt;, not text. Parse both documents, normalize, then compare the trees.&lt;/p&gt;

&lt;h1&gt;
  
  
  Three approaches, by where you're standing
&lt;/h1&gt;

&lt;h2&gt;
  
  
  1. Local triage: canonicalize, then diff
&lt;/h2&gt;

&lt;p&gt;For fast shell work, normalize first so plain &lt;code&gt;diff&lt;/code&gt; has a fair shot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diff &lt;span class="nt"&gt;-u&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;jq &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; payload-v1.json&lt;span class="o"&gt;)&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;jq &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; payload-v2.json&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-S&lt;/code&gt; sorts keys, so key order stops being a problem. Whitespace stops being a problem too, because &lt;code&gt;jq&lt;/code&gt; re-serializes both files in its own canonical style.&lt;/p&gt;

&lt;p&gt;Where it still fails: &lt;strong&gt;arrays&lt;/strong&gt;. Line-based diff compares array elements by position, so an unordered set coming back in a different order looks like three value substitutions instead of one permutation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Payload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;A&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"services"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"auth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"billing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gateway"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Payload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;B&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;same&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;set,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;different&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;order&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"services"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"billing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gateway"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need real structural comparison in the terminal, &lt;code&gt;jd&lt;/code&gt; does its own AST comparison and emits RFC 6902 JSON Patch operations (&lt;code&gt;add&lt;/code&gt;, &lt;code&gt;remove&lt;/code&gt;, &lt;code&gt;replace&lt;/code&gt;). It is the right tool when you want machine-readable output.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. CI/CD: pick a structural diff library
&lt;/h2&gt;

&lt;p&gt;In an integration test, asserting on two large payloads should not shell out. And &lt;code&gt;assert.deepStrictEqual()&lt;/code&gt; gives you an unreadable blob when both objects have several hundred keys.&lt;/p&gt;

&lt;p&gt;What you want is a library that understands structures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;deepdiff&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DeepDiff&lt;/span&gt;

&lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DeepDiff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;payload_v1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload_v2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ignore_order&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# treat arrays as unordered sets
&lt;/span&gt;    &lt;span class="n"&gt;significant_digits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# tolerate float drift
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node's &lt;code&gt;jsondiffpatch&lt;/code&gt; is the equivalent in the JS world. The important part is not which library — it is that you can express &lt;em&gt;what "equal" means for your domain&lt;/em&gt;: ignore the timestamp column, round the currency, treat that one array as a set.&lt;/p&gt;

&lt;p&gt;That last part is where line-based tools can never follow you.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Debugging in the browser: don't upload the payload at all
&lt;/h2&gt;

&lt;p&gt;This is where I got personal about it.&lt;/p&gt;

&lt;p&gt;Sometimes the payload you need to compare is a customer's production record, or something covered by a contract that says "not processed outside agreed infrastructure." Pasting it into a random online JSON tool is exactly the wrong move, and half the tools you would reach for are upload-by-design.&lt;/p&gt;

&lt;p&gt;So I built the thing I wanted: a JSON formatter and mapper that runs &lt;strong&gt;entirely in the browser&lt;/strong&gt;. One HTML file, no backend, nothing leaves your machine. It parses, compares and exports CSV client-side, and it works offline once you have the file.&lt;/p&gt;

&lt;p&gt;If you want to poke at it: &lt;a href="https://localjson-black.vercel.app/" rel="noopener noreferrer"&gt;LocalJSON&lt;/a&gt; — free tier with no account, Pro is a one-time $5 if you need the offline standalone version and unlimited runs.&lt;/p&gt;

&lt;p&gt;The comparison logic itself is worth understanding regardless of which tool you use, and there is a longer write-up with all the edge cases in my &lt;a href="https://localjson-black.vercel.app/blog/compare-json-files/" rel="noopener noreferrer"&gt;Comparing JSON Files&lt;/a&gt; piece — including the floating-point trap, which is nastier than the ordering one.&lt;/p&gt;

&lt;h1&gt;
  
  
  The part nobody warns you about
&lt;/h1&gt;

&lt;p&gt;Two edge cases that bite after you think you're done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Array ordering semantics.&lt;/strong&gt; RFC 8259 guarantees array order, but applications routinely use arrays for sets — permissions lists, database rows without an &lt;code&gt;ORDER BY&lt;/code&gt;. If you must compare such arrays, find a unique key (&lt;code&gt;id&lt;/code&gt;, &lt;code&gt;uuid&lt;/code&gt;) and correlate on that instead of by index. Positional comparison will lie to you forever otherwise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Floating-point precision.&lt;/strong&gt; JSON has no distinction between integer and float; it is just "number." Parsers then cast to IEEE 754 doubles, and runtimes disagree about how to print the result:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Value in memory&lt;/th&gt;
&lt;th&gt;Serialized JSON&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Python &lt;code&gt;json.dumps&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;float(1.0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node &lt;code&gt;JSON.stringify&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go &lt;code&gt;encoding/json&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;float64(1e-4)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.0001&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same number, three spellings. Any comparison that is not numeric-aware will report drift that does not exist.&lt;/p&gt;

&lt;h1&gt;
  
  
  Bottom line
&lt;/h1&gt;

&lt;p&gt;If you are diffing JSON, stop diffing JSON as text.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Quick local look → &lt;code&gt;jq -S&lt;/code&gt; + &lt;code&gt;diff&lt;/code&gt;, or &lt;code&gt;jd&lt;/code&gt; when you want a patch.&lt;/li&gt;
&lt;li&gt;Automated checks → &lt;code&gt;deepdiff&lt;/code&gt; / &lt;code&gt;jsondiffpatch&lt;/code&gt; with domain-specific equality rules.&lt;/li&gt;
&lt;li&gt;Sensitive payloads → keep the data in your browser and never upload it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole exercise costs you one insight: compare structure, not lines.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you end up trying this on your own payloads, I would genuinely like to hear which of the three approaches your team standardized on — especially whether "ignore this array's order" is a rule you have ever needed in CI.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>json</category>
      <category>diffing</category>
      <category>dbugdiarey</category>
    </item>
  </channel>
</rss>
