Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool

Safari Extension Development for JSON Formatting

Have you ever landed on a web page that displays raw JSON data, unformatted and difficult to read? As developers, we frequently encounter this. A browser extension that automatically detects and formats JSON can be a huge productivity booster. This guide will walk you through creating such an extension specifically for Safari.

Why Build a JSON Formatter Extension?

Raw JSON on a page is a common sight, especially when interacting with APIs or viewing data directly returned by a server. Without formatting, large JSON objects or arrays are just long strings of text, making it impossible to quickly understand their structure, find specific keys, or debug responses.

While developer tools in Safari offer network tabs where you can inspect JSON responses, sometimes the JSON is simply the entire content of the page (e.g., when you navigate directly to a JSON file URL or an API endpoint). An extension that formats the page content itself provides a seamless experience.

Safari Extension Architecture Basics (Manifest V3)

Safari, like other modern browsers, has adopted the Manifest V3 standard for extensions. This architecture emphasizes security, performance, and clear separation of concerns. Key components include:

  • Manifest File (`manifest.json`): The heart of your extension. It defines metadata (name, version), permissions required, and declares the various scripts and resources your extension uses.
  • Content Scripts: These scripts run in the context of specific web pages you define. They have access to the page's DOM but have limited access to background script APIs. This is where our JSON formatting logic will live.
  • Background Service Worker (or Persistent Background Page): Runs in the background, listening for browser events (like navigating to a new tab). It doesn't have direct access to the page DOM but can communicate with content scripts. For our simple formatter, a background script might not be strictly necessary if the logic is entirely within the content script, but it's crucial for more complex extensions.
  • Popup (Browser Action): An optional small UI window that appears when you click the extension's icon in the toolbar. We won't strictly need one for automatic formatting, but you could add controls here.

For a JSON formatter that modifies the content of the page itself, the primary focus will be on the Content Script.

Step 1: The Manifest File (`manifest.json`)

This file tells Safari about your extension. Here's a basic structure for our JSON formatter:

`manifest.json` example:

{
  "manifest_version": 3,
  "name": "JSON Formatter for Safari",
  "version": "1.0",
  "description": "Automatically formats JSON displayed directly in the browser.",
  "permissions": [
    "activeTab"
    // Or "scripting" and specify host permissions if needed for broader access
  ],
  "content_scripts": [
    {
      "matches": ["*://*/*"], // Match all http/https pages - refine this later
      "js": ["content.js"], // Your content script file
      "run_at": "document_idle" // Run after the page is mostly loaded
    }
  ]
  // Optional: Add background script if needed for more complex logic
  // "background": {
  //   "service_worker": "background.js"
  // }
}

Explanation:

  • manifest_version: 3: Specifies the architecture version.
  • name, version, description: Basic information.
  • permissions: Defines what your extension can do. "activeTab" is generally safer as it grants permission only when the user interacts with the extension on the current tab. For automatic formatting on specific URLs, you might need "scripting" along with "host_permissions" like {"*://*/*"}, but be mindful of the broad scope. Let's stick with "activeTab" for a simpler case or assume we'll refine `matches`.
  • content_scripts: An array defining scripts to inject into pages.
    • matches: URLs where the script should run. "*://*/*" is a wildcard for any HTTP/HTTPS page. You could refine this to specific patterns if you know the API endpoints or file types.
    • js: The list of JavaScript files to inject. We'll have one: content.js. (Note: If writing in TypeScript, you'll compile to JavaScript).
    • run_at: When the script injects. "document_idle" is usually safe, waiting for the page to settle.

Step 2: The Content Script (`content.ts`)

This is where the magic happens. The content script will run on matched pages. Its main tasks are:

  1. Detect JSON: Check if the page content is actually JSON.
  2. Parse JSON: Convert the raw text into a JavaScript object.
  3. Format & Display: Create structured HTML to represent the JSON nicely, optionally with collapsing/expanding sections.
  4. Replace Content: Swap the original page content with the formatted HTML.

Basic Content Script Structure:

// content.ts or content.js (after compilation)

// Function to check if the current page's content is likely JSON
function isJsonPage(): boolean {
  // A simple check: does the body content start with '{' or '[' ?
  // This is basic; more robust checks involve checking MIME type in background script
  // or more sophisticated content inspection.
  const bodyContent = document.body.textContent?.trim();
  return bodyContent !== undefined &&
         (bodyContent.startsWith('{') || bodyContent.startsWith('['));
}

// Function to format JSON text
function formatJson(jsonString: string): string {
  try {
    const jsonObject = JSON.parse(jsonString);
    // Use JSON.stringify with indentation for pretty printing
    return JSON.stringify(jsonObject, null, 2); // 2 spaces indentation
  } catch (e) {
    console.error("Failed to parse JSON:", e);
    return "Error: Invalid JSON data.";
  }
}

// Helper to escape HTML entities in the JSON string before putting it in <pre>
function escapeHtml(unsafe: string): string {
    return unsafe
         .replace(/&/g, "&amp;")
         .replace(/</g, "&lt;")
         .replace(/>/g, "&gt;")
         .replace(/"/g, "&quot;")
         .replace(/'/g, "&#039;");
}

// Function to create HTML structure for the formatted JSON
function createFormattedHtml(formattedJsonString: string): string {
  // This is a very basic example. A real formatter would build nested elements
  // for objects and arrays, add classes for styling, potentially add collapse/expand buttons.
  // For simplicity, we wrap preformatted text in a styled div.
  return `<div style="font-family: monospace; white-space: pre-wrap; word-break: break-all;">
            <pre>${escapeHtml(formattedJsonString)}</pre>
          </div>`;
}


// Main logic
if (isJsonPage()) {
  const rawJsonText = document.body.textContent?.trim();
  if (rawJsonText) {
    const formattedText = formatJson(rawJsonText);
    const formattedHtml = createFormattedHtml(formattedText);

    // Clear the original page content and insert the formatted HTML
    document.body.innerHTML = formattedHtml;

    // Optional: Inject CSS for better styling
    injectCss();
  }
}

// Function to inject basic CSS (implemented in the next step)
function injectCss() {
  // This will be implemented in Step 3
  console.log("Injecting CSS...");
}

Refining JSON Detection: The isJsonPage() function above is very basic. A more robust approach might involve:

  • Checking the Content-Type HTTP header. This typically requires a background script or using the webRequest API (if allowed by Manifest V3 in Safari).
  • Attempting a JSON.parse within a try{...}catch block on the page content. If it succeeds, it's likely valid JSON.
  • Looking for specific HTML structures if the JSON is embedded within HTML (less common for raw JSON pages).

Step 3: Adding Styling (`style.css`)

The formatted JSON will look better with some CSS. You can inject CSS in a few ways:

  • Directly inject a <style> tag into the head of the document from your content script.
  • Declare CSS files in `manifest.json` within the `content_scripts` section (Safari handles this injection automatically). This is the preferred method.

Let's go with the manifest.json approach. First, create a simple CSS file (`style.css`):

`style.css` example:

body {
  background-color: #f4f4f4; /* Light background */
  color: #333; /* Dark text */
  line-height: 1.6;
  padding: 20px;
}

pre {
  background-color: #fff; /* White background for code block */
  padding: 15px;
  border-radius: 8px;
  border: 1px solid #ddd;
  overflow-x: auto; /* Add horizontal scroll for long lines */
}

/* Basic syntax highlighting (conceptual - requires more complex logic) */
.json-key { color: #a52a2a; } /* Brown */
.json-string { color: #006400; } /* Dark Green */
.json-number { color: #0000cd; } /* Medium Blue */
.json-boolean { color: #8a2be2; } /* Blue Violet */
.json-null { color: #a0a0a0; } /* Grey */
.json-punctuation { color: #333; } /* Dark grey */

Then, update your `manifest.json` to include this CSS file:

Updated `manifest.json` (Content Scripts section):

  "content_scripts": [
    {
      "matches": ["*://*/*"],
      "js": ["content.js"],
      "css": ["style.css"],
      "run_at": "document_idle"
    }
  ],

For actual syntax highlighting, you would need to traverse the parsed JSON object and build the HTML structure with nested <span> elements having classes like `.json-key`, `.json-string`, etc., and then apply CSS rules to those classes. This is a more advanced step for building the HTML output in `createFormattedHtml`.

Challenges and Considerations

  • Single Page Applications (SPAs): If a website is a SPA, the URL might change or new content (including JSON) might be loaded dynamically without a full page refresh. Your content script, injected at document_idle, might only run once. You'd need to observe DOM changes (using MutationObserver) or listen for messages from a background script triggered by URL changes to re-check for JSON content.
  • Large JSON Data: Parsing and rendering extremely large JSON files can cause performance issues or even crash the tab. Consider adding limits or lazy-loading/rendering strategies for very large inputs.
  • Security (XSS): If you directly insert user-provided or remote content (like the JSON string) into the DOM using innerHTML, you MUST properly escape any HTML special characters (`<`, `>`, `&`, `"`, `'`). The escapeHtml helper in the example is a basic step towards this, but building the DOM structure programmatically using document.createElementand textContent is generally safer than manipulating innerHTML with escaped strings.
  • MIME Type Check: Relying solely on content sniffing (startsWith('{') or startsWith('[')) isn't foolproof. Checking the Content-Type header (like application/json) is the standard way to confirm if a page is serving JSON. This often requires a background script listening to webRequest events to inspect headers *before* the page content loads.
  • Interfering with Websites: Your content script runs on potentially *any* website. Using overly broad matches patterns and manipulating document.body.innerHTML can break websites that aren't raw JSON pages. Refine your matches and add more robust checks in isJsonPage. Alternatively, consider making the formatter opt-in (e.g., via a browser action button click) rather than automatic.

Packaging and Testing in Safari

Unlike Chrome or Firefox, building and installing Safari extensions typically involves Xcode.

  1. Open Xcode.
  2. Go to File > New > Project...
  3. Select the "macOS" tab, then choose the "Safari Extension" template.
  4. Follow the prompts to create the project.
  5. In the project navigator, you'll find a folder structure. Typically, you'll place your `manifest.json`, `content.js` (compiled from `content.ts`), and `style.css` files within the appropriate extension bundle or source directory as defined by the Xcode project setup. Xcode is the environment that bundles these files correctly for Safari.
  6. Write your `content.ts` and `style.css`. Use a TypeScript compiler (`tsc`) to compile `content.ts` into `content.js`.
  7. In Safari, enable "Show Develop menu in menu bar" (Safari > Settings > Advanced).
  8. In the Develop menu, select "Allow Unsigned Extensions".
  9. Build and run your extension project from Xcode. This should install and enable your extension in Safari.
  10. Navigate to a page that serves raw JSON (you can find examples online or create a local test file). If your detection and formatting logic are correct, the page content should be replaced by your nicely formatted JSON view.

Xcode handles the complexities of signing and packaging for distribution (App Store) or for local testing. For development, the "Allow Unsigned Extensions" combined with running directly from Xcode is the standard workflow.

Conclusion

Building a Safari extension for JSON formatting is a practical way to learn about browser extension development, focusing on content scripts and interacting with page content. While this guide covers the core concepts and a basic implementation approach, a production-ready extension would involve more robust JSON detection, advanced formatting (syntax highlighting, collapse/expand features), better error handling, and careful consideration of performance and compatibility across various websites.

Experiment with the content script logic, explore ways to improve the HTML structure and styling, and delve deeper into the Safari extension documentation and Manifest V3 specifics to enhance your formatter or build more complex browser tools!

Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool