OpenViSTAI · Agents · Blockchain · Crypto
Menu
OPENVIST RECIPES

Compare two JSON objects and report changed paths with Python

Python

Find added, removed and changed JSON object paths without fetching an API or altering either input.

Python code excerpt: Compare two JSON objects and report changed paths with Python. Version 0.1.0. Read the full recipe for code, sources and recorded test scope.

Compare two saved JSON objects without contacting an API or changing either file. Report sorted paths for added, removed and changed values. Objects are compared recursively; arrays are treated as complete values.

Before you start

  • Python 3.12 on Linux; standard library only.
  • Two UTF-8 regular files containing top-level JSON objects.
  • At most 1 MiB per file. Final-component symlinks are rejected.

Build and check

  1. Save the example code as compare_json.py.
  2. Save the original object as before.json and the new object as after.json.
  3. Run: python3 compare_json.py before.json after.json
  4. Inspect the added, removed and changed arrays. Validation errors exit with code 2.

Limits to understand

  • Linux-specific open flags. Rejects final-component symlinks, directories and non-regular files. Intermediate directories may contain symlinks; use trusted paths. This is not a host filesystem sandbox.
  • Deeply nested inputs or unusually long integer literals may exceed Python runtime limits and are rejected with exit 2.
  • Integers and finite floats use Python numeric equality, so 1 and 1.0 compare equal. Floating-point parsing follows Python's rounding behavior.
  • Both documents are parsed in memory. Memory use can exceed their combined file sizes.
  • This output is a difference summary, not a JSON Patch or a lossless reconstruction.
  • Arrays are atomic: a changed element reports the array path, not its index.
  • Added or removed objects report their own path without expanding descendants.

Run it

python3 compare_json.py before.json after.json

Example input

before.json: {"name":"A","enabled":true}
after.json: {"name":"B","enabled":true,"count":2}

Expected output

{"added": ["/count"], "removed": [], "changed": ["/name"]}
INSPECT THE SCRIPT

Inspect, copy and run the documented checks.

This browser preview builds code locally. It does not contact an API or run the code.

Default example ready.

Python example
import argparse
import json
import os
import stat

MAX_BYTES = 1024 * 1024


class ValidationError(ValueError):
    pass


def reject_constant(token):
    raise ValidationError('Non-finite JSON constant is not allowed: ' + token)


def parse_finite_float(token):
    value = float(token)
    if value == float('inf') or value == float('-inf'):
        raise ValidationError('JSON number overflows to an infinite float')
    return value


def unique_object(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValidationError('Duplicate object key: ' + json.dumps(key))
        result[key] = value
    return result


def load_object(path):
    # Linux: do not follow the final symlink or block opening a FIFO.
    descriptor = os.open(path, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW | os.O_CLOEXEC)
    try:
        if not stat.S_ISREG(os.fstat(descriptor).st_mode):
            raise ValidationError('Input must be a regular local file')
        with os.fdopen(descriptor, 'rb', closefd=False) as stream:
            data = stream.read(MAX_BYTES + 1)
    finally:
        os.close(descriptor)
    if len(data) > MAX_BYTES:
        raise ValidationError('Input exceeds the 1 MiB limit')
    text = data.decode('utf-8')
    value = json.loads(
        text,
        object_pairs_hook=unique_object,
        parse_constant=reject_constant,
        parse_float=parse_finite_float,
    )
    if not isinstance(value, dict):
        raise ValidationError('Top-level JSON value must be an object')
    return value


def equal_values(left, right):
    if isinstance(left, bool) or isinstance(right, bool):
        return isinstance(left, bool) and isinstance(right, bool) and left == right
    if isinstance(left, dict) or isinstance(right, dict):
        if not isinstance(left, dict) or not isinstance(right, dict):
            return False
        if left.keys() != right.keys():
            return False
        return all(equal_values(left[key], right[key]) for key in left)
    if isinstance(left, list) or isinstance(right, list):
        if not isinstance(left, list) or not isinstance(right, list):
            return False
        return len(left) == len(right) and all(
            equal_values(a, b) for a, b in zip(left, right)
        )
    if isinstance(left, (int, float)) or isinstance(right, (int, float)):
        return (
            isinstance(left, (int, float))
            and isinstance(right, (int, float))
            and left == right
        )
    if left is None or right is None:
        return left is None and right is None
    return isinstance(left, str) and isinstance(right, str) and left == right


def child_pointer(parent, key):
    return parent + '/' + key.replace('~', '~0').replace('/', '~1')


def compare_objects(left, right, parent, result):
    for key in left:
        path = child_pointer(parent, key)
        if key not in right:
            result['removed'].append(path)
        elif isinstance(left[key], dict) and isinstance(right[key], dict):
            compare_objects(left[key], right[key], path, result)
        elif not equal_values(left[key], right[key]):
            result['changed'].append(path)
    for key in right:
        if key not in left:
            result['added'].append(child_pointer(parent, key))


def main():
    parser = argparse.ArgumentParser(
        description='Compare two UTF-8 JSON objects without modifying either input.'
    )
    parser.add_argument('before', help='Original JSON file')
    parser.add_argument('after', help='New JSON file')
    args = parser.parse_args()
    try:
        before = load_object(args.before)
        after = load_object(args.after)
        result = {'added': [], 'removed': [], 'changed': []}
        compare_objects(before, after, '', result)
        for paths in result.values():
            paths.sort()
        output = json.dumps(result, ensure_ascii=True, allow_nan=False)
    except (OSError, ValueError, RecursionError, OverflowError) as error:
        parser.exit(2, 'error: ' + str(error) + '\n')
    print(output)
    return 0


if __name__ == '__main__':
    raise SystemExit(main())
What to verify after running
  • Six independent fixtures passed in an offline CPython 3.12.15 Docker container.
  • Checked identical objects, additions/removals, nested changes, escaped paths, boolean-versus-number arrays and duplicate-key rejection.
  • The fixture input files remained unchanged.

Request or format preview

Illustrative preview. No provider response, price or balance is fetched.

Change a setting to inspect the preview.

Official sources and related profiles

    Code license: MIT. Keep the license notice with your copy.

    Example version 0.1.0 · Documentation reviewed 2026-10-14.

    Six independent CLI fixtures passed in pinned, offline CPython 3.12.15 Docker. Additional Linux regression checks passed as part of the VPS test suite, covering size boundaries, invalid JSON, non-finite values and non-regular files. This does not establish behavior on other operating systems or exhaustive correctness.

    Related browser tools

    Inspect related data locally in your browser. These tools have their own input rules and limits; they do not execute this recipe.