Compare JSON

Comparing two JSON documents side by side.

The app presents two text areas arranged in left and right columns. A user can paste a JSON string into each text area and click a Compare button. The script then attempts to parse, normalize, and pretty-format both JSON values, updates the displayed text areas with the formatted JSON, and reports whether the two JSON documents are equivalent.

import json
from typing import Any

import streamlit as st


MAX_DIFFS = 200
load_json(raw_text: str) tuple[Any | None, str | None]
def load_json(raw_text: str) -> tuple[Any | None, str | None]:
    if not raw_text.strip():
        return None, "JSON input is empty."

    try:
        return json.loads(raw_text), None
    except json.JSONDecodeError as exc:
        return None, f"Invalid JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}"
format_json(value: Any) str
def format_json(value: Any) -> str:
    return json.dumps(value, indent=2, sort_keys=True, ensure_ascii=False)
preview_value(value: Any) str
def preview_value(value: Any) -> str:
    rendered = json.dumps(value, sort_keys=True, ensure_ascii=False)
    return rendered if len(rendered) <= 140 else f"{rendered[:137]}..."
path_join(path: str, part: str) str
def path_join(path: str, part: str) -> str:
    return f"{path}.{part}" if path else part
compare_json(left: Any, right: Any, path: str = '$') list[str]
def compare_json(left: Any, right: Any, path: str = "$") -> list[str]:
    diffs: list[str] = []

    def add(message: str) -> bool:
        diffs.append(message)
        return len(diffs) >= MAX_DIFFS

    def walk(left_value: Any, right_value: Any, current_path: str) -> bool:
        if type(left_value) is not type(right_value):
            return add(
                f"{current_path}: type differs "
                f"({type(left_value).__name__} != {type(right_value).__name__})"
            )

        if isinstance(left_value, dict):
            left_keys = set(left_value)
            right_keys = set(right_value)

            for key in sorted(left_keys - right_keys):
                if add(f"{path_join(current_path, key)}: only in left"):
                    return True

            for key in sorted(right_keys - left_keys):
                if add(f"{path_join(current_path, key)}: only in right"):
                    return True

            for key in sorted(left_keys & right_keys):
                if walk(left_value[key], right_value[key], path_join(current_path, key)):
                    return True

            return False

        if isinstance(left_value, list):
            common_length = min(len(left_value), len(right_value))

            for index in range(common_length):
                if walk(left_value[index], right_value[index], f"{current_path}[{index}]"):
                    return True

            if len(left_value) > len(right_value):
                for index in range(common_length, len(left_value)):
                    if add(f"{current_path}[{index}]: only in left"):
                        return True

            if len(right_value) > len(left_value):
                for index in range(common_length, len(right_value)):
                    if add(f"{current_path}[{index}]: only in right"):
                        return True

            return False

        if left_value != right_value:
            return add(
                f"{current_path}: value differs "
                f"({preview_value(left_value)} != {preview_value(right_value)})"
            )

        return False

    walk(left, right, path)
    return diffs
compare_inputs() None
def compare_inputs() -> None:
    left_json, left_error = load_json(st.session_state.left_json)
    right_json, right_error = load_json(st.session_state.right_json)

    st.session_state.left_error = left_error
    st.session_state.right_error = right_error
    st.session_state.diffs = []
    st.session_state.are_equal = False

    if left_error or right_error:
        return

    st.session_state.left_json = format_json(left_json)
    st.session_state.right_json = format_json(right_json)
    st.session_state.diffs = compare_json(left_json, right_json)
    st.session_state.are_equal = not st.session_state.diffs


st.set_page_config(page_title="JSON Compare", layout="wide")
st.title("JSON Compare")

if "left_json" not in st.session_state:
    st.session_state.left_json = ""
if "right_json" not in st.session_state:
    st.session_state.right_json = ""
if "left_error" not in st.session_state:
    st.session_state.left_error = None
if "right_error" not in st.session_state:
    st.session_state.right_error = None
if "diffs" not in st.session_state:
    st.session_state.diffs = []
if "are_equal" not in st.session_state:
    st.session_state.are_equal = False

left_column, right_column = st.columns(2)

with left_column:
    st.text_area("Left JSON", key="left_json", height=520)

with right_column:
    st.text_area("Right JSON", key="right_json", height=520)

st.button("Compare", type="primary", on_click=compare_inputs)

if st.session_state.left_error:
    st.error(f"Left JSON: {st.session_state.left_error}")

if st.session_state.right_error:
    st.error(f"Right JSON: {st.session_state.right_error}")

if not st.session_state.left_error and not st.session_state.right_error:
    if st.session_state.are_equal:
        st.success("JSON documents are equal after normalization.")
    elif st.session_state.diffs:
        st.warning(f"JSON documents differ. Showing up to {MAX_DIFFS} differences.")
        st.code("\n".join(st.session_state.diffs), language="text")