or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

array-diffing.mdcharacter-diffing.mdcss-diffing.mdcustom-diffing.mdformat-conversion.mdindex.mdjson-diffing.mdline-diffing.mdpatch-application.mdpatch-creation.mdpatch-utilities.mdsentence-diffing.mdword-diffing.md
tile.json

json-diffing.mddocs/

JSON Diffing

JSON object comparison with pretty-printing and custom serialization options. Automatically serializes objects to prettified JSON format and performs line-based comparison for readable diff output.

Capabilities

diffJson Function

Performs JSON-aware diff between two objects by serializing them to pretty-printed JSON.

/**
 * Compare two JSON-serializable objects
 * @param oldObj - Original object or JSON string
 * @param newObj - New object to compare against
 * @param options - Configuration options
 * @returns Array of change objects representing the diff
 */
function diffJson(oldObj, newObj, options);

Options:

interface JsonDiffOptions extends DiffOptions {
  stringifyReplacer?: (key: string, value: any) => any;  // Custom JSON.stringify replacer
  undefinedReplacement?: any;  // Value to replace undefined properties
}

Usage Examples:

import { diffJson } from "diff";

// Basic object comparison
const result = diffJson(
  { name: "Alice", age: 25, city: "New York" },
  { name: "Alice", age: 26, city: "Boston" }
);
console.log(result);
// Shows line-by-line diff of pretty-printed JSON

// Array comparison  
const arrayResult = diffJson(
  { items: [1, 2, 3] },
  { items: [1, 2, 3, 4] }
);

// Nested object comparison
const nestedResult = diffJson(
  {
    user: {
      profile: { name: "John", settings: { theme: "dark" } }
    }
  },
  {
    user: {
      profile: { name: "John", settings: { theme: "light", lang: "en" } }
    }
  }
);

canonicalize Function

Recursively canonicalizes objects for consistent JSON serialization, handling circular references.

/**
 * Canonicalize object for consistent JSON comparison
 * @param obj - Object to canonicalize
 * @param stack - Internal recursion stack (optional)
 * @param replacementStack - Internal replacement stack (optional)
 * @param replacer - Custom replacer function (optional)
 * @param key - Current property key (optional)
 * @returns Canonicalized object with sorted keys
 */
function canonicalize(obj, stack, replacementStack, replacer, key);

jsonDiff Instance

Pre-configured Diff instance for JSON comparisons with specialized handling.

/**
 * Pre-configured JSON diff instance
 * Uses line-based tokenization of pretty-printed JSON
 * Includes longest token preference for better diff quality
 */
const jsonDiff: Diff;

Advanced Usage

Custom Serialization

import { diffJson } from "diff";

// Custom replacer function
const result = diffJson(
  { date: new Date("2023-01-01"), value: undefined },
  { date: new Date("2023-01-02"), value: null },
  {
    stringifyReplacer: (key, value) => {
      if (value instanceof Date) {
        return value.toISOString();
      }
      if (value === undefined) {
        return "<undefined>";
      }
      return value;
    }
  }
);

// Undefined replacement
const undefinedResult = diffJson(
  { a: 1, b: undefined, c: 3 },
  { a: 1, b: 2, c: undefined },
  {
    undefinedReplacement: null
  }
);

Object Preprocessing

import { diffJson, canonicalize } from "diff";

// Manual canonicalization
const obj1 = { b: 2, a: 1, c: { z: 26, y: 25 } };
const obj2 = { a: 1, b: 3, c: { y: 25, z: 26 } };

const canonical1 = canonicalize(obj1);
const canonical2 = canonicalize(obj2);
console.log("Canonicalized objects have consistent property order");

// Direct comparison with canonicalized objects
const result = diffJson(canonical1, canonical2);

API Response Comparison

import { diffJson } from "diff";

function compareApiResponses(response1, response2) {
  // Filter out volatile fields before comparison
  const cleaned1 = cleanResponse(response1);
  const cleaned2 = cleanResponse(response2);
  
  return diffJson(cleaned1, cleaned2, {
    stringifyReplacer: (key, value) => {
      // Ignore timestamp fields
      if (key.includes('timestamp') || key.includes('time')) {
        return '<timestamp>';
      }
      // Normalize IDs
      if (key === 'id' && typeof value === 'string') {
        return '<id>';
      }
      return value;
    }
  });
}

function cleanResponse(response) {
  const { timestamp, requestId, ...cleaned } = response;
  return cleaned;
}

// Usage
const oldResponse = {
  status: "success",
  data: { users: [{ id: "123", name: "Alice" }] },
  timestamp: "2023-01-01T10:00:00Z"
};

const newResponse = {
  status: "success", 
  data: { users: [{ id: "456", name: "Alice" }, { id: "789", name: "Bob" }] },
  timestamp: "2023-01-01T10:05:00Z"
};

const apiDiff = compareApiResponses(oldResponse, newResponse);

Configuration Comparison

import { diffJson } from "diff";

function compareConfigurations(oldConfig, newConfig) {
  const changes = diffJson(oldConfig, newConfig);
  
  const analysis = {
    addedKeys: [],
    removedKeys: [],
    modifiedValues: [],
    unchangedLines: 0
  };
  
  changes.forEach(change => {
    if (change.added) {
      const keyMatch = change.value.match(/^\s*"([^"]+)":/);
      if (keyMatch) {
        analysis.addedKeys.push(keyMatch[1]);
      }
    } else if (change.removed) {
      const keyMatch = change.value.match(/^\s*"([^"]+)":/);
      if (keyMatch) {
        analysis.removedKeys.push(keyMatch[1]);
      }
    } else {
      analysis.unchangedLines++;
    }
  });
  
  return analysis;
}

// Usage for configuration management
const oldConfig = {
  database: { host: "localhost", port: 5432 },
  cache: { enabled: true, ttl: 3600 }
};

const newConfig = {
  database: { host: "prod-db", port: 5432, ssl: true },
  cache: { enabled: false, ttl: 7200 },
  logging: { level: "info" }
};

const configAnalysis = compareConfigurations(oldConfig, newConfig);

Large Object Handling

import { diffJson } from "diff";

function diffLargeObjects(obj1, obj2, callback) {
  diffJson(obj1, obj2, {
    callback: callback,
    maxEditLength: 15000,  // Suitable for large JSON objects
    timeout: 25000,        // 25 second timeout
    stringifyReplacer: (key, value) => {
      // Potentially exclude large nested objects
      if (key === 'largeData' && Array.isArray(value) && value.length > 1000) {
        return `<large array: ${value.length} items>`;
      }
      return value;
    }
  });
}

// Usage
diffLargeObjects(largeObject1, largeObject2, (result) => {
  if (result) {
    const changeLines = result.filter(r => r.added || r.removed).length;
    console.log(`JSON diff completed: ${changeLines} changed lines`);
  } else {
    console.log("Objects too different to compute JSON diff efficiently");
  }
});

Circular Reference Handling

import { diffJson, canonicalize } from "diff";

// The library automatically handles circular references
const objWithCircular = { name: "test" };
objWithCircular.self = objWithCircular;

const anotherObj = { name: "test", other: "value" };

// This works safely - circular references are handled
const result = diffJson(objWithCircular, anotherObj);

// Manual canonicalization also handles circularity
const canonical = canonicalize(objWithCircular);
console.log("Circular reference handled:", canonical);

Direct Instance Usage

import { jsonDiff } from "diff";

// Using the pre-configured instance
const directResult = jsonDiff.diff(
  { old: "value" },
  { new: "value" }
);

// Access the JSON serialization behavior
const serialized = jsonDiff.castInput({ b: 2, a: 1 });
console.log("Serialized JSON:", serialized);
// Pretty-printed JSON with sorted keys

// Custom equality (handles trailing commas in JSON)
const isEqual = jsonDiff.equals(
  '  "key": "value",',
  '  "key": "value"'
);
console.log("Ignores trailing comma differences:", isEqual);

Data Migration Tracking

import { diffJson } from "diff";

function trackDataMigration(beforeMigration, afterMigration) {
  const diff = diffJson(beforeMigration, afterMigration);
  
  const migrationReport = {
    totalChanges: diff.filter(d => d.added || d.removed).length,
    dataIntegrityIssues: [],
    schemaChanges: [],
    valueTransformations: []
  };
  
  diff.forEach(change => {
    if (change.added || change.removed) {
      const line = change.value.trim();
      
      // Detect schema changes
      if (line.includes('"type":') || line.includes('"schema":')) {
        migrationReport.schemaChanges.push({
          change: change.added ? 'added' : 'removed',
          content: line
        });
      }
      
      // Detect potential data loss
      if (change.removed && line.includes(':') && !line.includes('null')) {
        migrationReport.dataIntegrityIssues.push({
          type: 'potential_data_loss',
          content: line
        });
      }
    }
  });
  
  return migrationReport;
}

Testing and Validation

import { diffJson } from "diff";

function validateJsonChanges(expected, actual) {
  const changes = diffJson(expected, actual);
  
  const validation = {
    isMatch: changes.every(change => !change.added && !change.removed),
    differences: changes.filter(change => change.added || change.removed),
    summary: {
      additions: changes.filter(c => c.added).length,
      removals: changes.filter(c => c.removed).length
    }
  };
  
  return validation;
}

// Usage in tests
const expectedResult = { status: "success", count: 10 };
const actualResult = { status: "success", count: 12 };
const validation = validateJsonChanges(expectedResult, actualResult);

if (!validation.isMatch) {
  console.log("Test failed with differences:", validation.differences);
}