JSON object diff
Put the old and new version of a data record side by side and get the real differences sorted out: in the example, 2 additions, 1 removal and 2 changes, 5 in total, while spacing and key order are ignored. Free in your browser.
- Free
- Runs in your browser
- No sign-up
- Your data stays on your device
Run — free
Runs in your browser. Free, unlimited — your data never leaves this page.
Arrays remain ordered values and are compared as complete units. The operation is deterministic, requires no network access, and rejects arrays, primitives, or null when either top-level input must be an object.
How to read the list of differences
The result separates differences into added, removed, and changed collections. An added entry is a key that exists only in the updated object, while a removed entry is a key found only in the original. A changed entry exists on both sides but has a different value, so it includes both the before and after values.
For example, /profile/name identifies the name property inside profile. Pointer escaping keeps unusual property names precise: a slash inside a key becomes ~1 and a tilde becomes ~0. This is safer than dot notation because a literal key named profile.name cannot be confused with a nested name property.
Results are sorted by property name during traversal, making repeated calls with the same logical objects stable and easy to test. The counts block summarizes each category and provides a total, which is useful when a pipeline only needs to decide whether any meaningful difference exists before inspecting details. Empty arrays in all three categories mean the two objects are structurally equal, even if their original textual formatting or object-key order differed.
How nested objects and arrays are handled
When the same key contains an object on both sides, comparison continues recursively so the output points to the deepest differing property. If profile exists in both inputs and only profile.active changes, the changed path is /profile/active rather than the broader /profile. When a key exists on only one side, the result records that key once with its complete value; it does not expand every descendant into a separate addition or removal.
That choice keeps a newly inserted configuration section readable as one event. Any insertion, deletion, reordering, or element update therefore produces one changed entry for the array property, with complete before and after arrays. Primitive values and null are compared directly, while objects inside arrays participate in the array comparison without creating separate indexed paths.
If a property's type changes, such as an object becoming a string, that property is reported as changed.
Using the diff in reviews and automation
Send the original object in before and the proposed object in after. A deployment check can reject changes when removed contains protected settings, a test can assert that only approved paths changed, and an audit process can store the compact difference instead of duplicating two large documents. They should also inspect the category, not only the path: removing a value and adding one elsewhere carries different operational meaning from changing an existing value.
For arrays, downstream code should expect the whole array in a changed record and apply any domain-specific set or identity logic separately.
What you can do with it
Review configuration releases
Compare the deployed configuration with a candidate release and surface only structural key changes, without whitespace or key-order noise.
Guard approved settings
Check diff paths in CI and stop a deployment when protected settings are removed or altered outside an approved change list.
FAQ
What path format does the result use?
Slashes in property names are escaped as ~1 and tildes as ~0.
Does object key order count as a change?
No. Objects are compared by their keys and values, so different serialization order does not produce a difference.
How are arrays compared?
Arrays are ordered values and are compared as complete units. Any element edit, insertion, deletion, or reordering marks the containing array property as changed.
Can I compare two top-level arrays?
A top-level array or primitive produces an invalid-input error.
Does a change deep inside a section show up on its own?
Yes. When both versions contain the same section, the comparison goes inside it and points to the deepest entry that differs. In the example only active changed inside profile, so the result names /profile/active rather than the whole profile.
Related tools
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, by email and from Telegram — and soon from our app too.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/data/diff-json-objects \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"before":{"name":"Ada","profile":{"active":true,"score":8},"roles":["reader"]},"after":{"name":"Ada","profile":{"active":false,"score":8,"city":"London"},"roles":["reader","editor"]}}'const res = await fetch("https://api.kit.forhosting.com/data/diff-json-objects", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"before": {
"name": "Ada",
"profile": {
"active": true,
"score": 8
},
"roles": [
"reader"
]
},
"after": {
"name": "Ada",
"profile": {
"active": false,
"score": 8,
"city": "London"
},
"roles": [
"reader",
"editor"
]
}
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/data/diff-json-objects",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"before": {
"name": "Ada",
"profile": {
"active": true,
"score": 8
},
"roles": [
"reader"
]
},
"after": {
"name": "Ada",
"profile": {
"active": false,
"score": 8,
"city": "London"
},
"roles": [
"reader",
"editor"
]
}
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/data/diff-json-objects", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"before":{"name":"Ada","profile":{"active":true,"score":8},"roles":["reader"]},"after":{"name":"Ada","profile":{"active":false,"score":8,"city":"London"},"roles":["reader","editor"]}}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"before":{"name":"Ada","profile":{"active":true,"score":8},"roles":["reader"]},"after":{"name":"Ada","profile":{"active":false,"score":8,"city":"London"},"roles":["reader","editor"]}}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/data/diff-json-objects", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"before": {
"name": "Ada",
"profile": {
"active": true,
"score": 8
},
"roles": [
"reader"
]
},
"after": {
"name": "Ada",
"profile": {
"active": false,
"score": 8,
"city": "London"
},
"roles": [
"reader",
"editor"
]
}
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "data.diff_json_objects",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Limits
max_mb | 25 |
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |
Technical notes
A text diff can make a small JSON update look noisy because indentation, key order, and unrelated formatting changes distract from the data itself. This JSON object diff compares parsed values instead. It reports added, removed, and changed keys in separate lists, follows shared objects into nested properties, and gives every result an unambiguous JSON Pointer path.
Reading a structural JSON diff
Each entry uses a JSON Pointer path beginning with a slash.
How nested objects and arrays are handled
Arrays are treated as ordered JSON values rather than as objects keyed by index. Only the two top-level values have a stricter rule: both must be non-null JSON objects, because this capability is designed specifically for object-key differences rather than general document or array comparison.
Using the diff in reviews and automation
The browser tool can run locally for quick inspection, while an API request costs $0.002 and returns the same deterministic structure for automation. Consumers should match paths as JSON Pointers rather than splitting them naively on slashes, since escaped characters have defined meanings. Validate input before submitting when possible, but failed requests caused by a top-level array, null, string, number, or boolean return an invalid-input error rather than a misleading empty diff.
Integration use cases
Audit webhook payload evolution
Record which nested fields appeared, disappeared, or changed between two normalized webhook examples as an integration evolves.
Technical questions
How much does the API request cost?
The browser tool runs free locally. Each API request uses the published base price of $0.002, with no network or AI dependency in the comparison itself.