← Back to blog

Demystifying the Jupyter Notebook Format: JSON Schema and Cell Output Parsing

PetexSpace

The Jupyter Notebook (.ipynb) has become the standard document exchange format for computational science, machine learning research, and exploratory data analysis. Despite its visual presentation as an interactive, flowing computational laboratory notebook, a .ipynb file is fundamentally a structured JSON document governed by a formal JSON Schema standard known as nbformat.

Parsing, validating, and converting notebook files outside of traditional Python server runtimes requires understanding the schema's polymorphic cell structures, dynamic MIME-type hierarchies, terminal stream encodings, and stored script injection risks.

Understanding how to construct deterministic abstract syntax trees (AST) from raw notebook payloads enables high-fidelity conversions into Markdown, LaTeX, and PDF formats using client-side processing.

1. The Anatomy of nbformat: Top-Level Schema Specification

The Jupyter notebook format evolved from early IPython notebook drafts (v1 to v3) into the modern standard, nbformat Version 4 (currently v4.5). Unlike loosely typed document dumps, an nbformat document adheres to a strict JSON Schema definition that enforces strict structural invariants.

+-----------------------------------------------------------------------+
|                    nbformat Root JSON Document Tree                   |
+-----------------------------------------------------------------------+
|  Root Object                                                          |
|  |                                                                    |
|  +-- "nbformat": 4 (Integer, major format version)                    |
|  +-- "nbformat_minor": 5 (Integer, minor format incremental version)  |
|  |                                                                    |
|  +-- "metadata": {                                                    |
|  |     "kernelspec": {                                                |
|  |       "name": "python3",                                           |
|  |       "display_name": "Python 3 (ipykernel)",                      |
|  |       "language": "python"                                         |
|  |     },                                                             |
|  |     "language_info": {                                             |
|  |       "name": "python",                                            |
|  |       "version": "3.11.4",                                         |
|  |       "mimetype": "text/x-python",                                 |
|  |       "file_extension": ".py"                                      |
|  |     }                                                              |
|  |   }                                                                |
|  |                                                                    |
|  +-- "cells": [                                                       |
|        Polymorphic Array of Cell Primitives                           |
|        - Cell 0: Markdown Cell                                        |
|        - Cell 1: Code Cell                                            |
|        - Cell 2: Raw Cell                                             |
|      ]                                                                |
+-----------------------------------------------------------------------+

Schema Validation Invariants

A conforming nbformat parser must validate three root-level properties before processing document contents:

  1. nbformat: An integer defining the major version. If a parser targeting Version 4 encounters Version 3, it must run a migration transform or reject the file.
  2. nbformat_minor: An integer indicating backward-compatible feature additions (such as cell identifiers added in v4.5).
  3. cells: An ordered array of polymorphic cell objects.
  4. metadata: A dictionary holding runtime execution context, kernelspec configurations, language grammar identifiers, and arbitrary user-defined metadata.
// JSON Schema interface definition for nbformat v4.5 root document
export interface NotebookDocument {
  nbformat: 4;
  nbformat_minor: number;
  metadata: {
    kernelspec?: {
      name: string;
      display_name: string;
      language?: string;
    };
    language_info?: {
      name: string;
      version?: string;
      mimetype?: string;
      file_extension?: string;
      pygments_lexer?: string;
    };
    authors?: Array<{ name: string }>;
    [key: string]: unknown;
  };
  cells: NotebookCell[];
}

If any top-level key is omitted or contains incorrect data types, parsing halts. When building tools that run inside modern web browsers, schema validation provides the first line of defense against corrupted payloads or malicious data structures.

2. Cell Polymorphism: Code, Markdown, and Raw Payloads

The core computational narrative of a notebook resides in its cells array. Each cell is a tagged union differentiated by the cell_type string discriminator.

+-----------------------------------------------------------------------+
|                       Polymorphic Cell Variants                       |
+-----------------------------------------------------------------------+
|  BaseCell { id, cell_type, metadata }                                 |
|       |                                                               |
|       +---> MarkdownCell                                              |
|       |     - "source": string | string[]                             |
|       |     - "attachments": Record<string, MimeBundle>               |
|       |                                                               |
|       +---> CodeCell                                                  |
|       |     - "source": string | string[]                             |
|       |     - "execution_count": number | null                        |
|       |     - "outputs": Array<CellOutput>                            |
|       |                                                               |
|       +---> RawCell                                                   |
|             - "source": string | string[]                             |
|             - "metadata": { "format": "text/latex" | "text/html" }    |
+-----------------------------------------------------------------------+

The Three Core Cell Primitives

  • Markdown Cells (markdown): Contain explanatory narrative formatted using GitHub-Flavored Markdown syntax, inline LaTeX equations (E=mc2E = mc^2), display equations, and optional image attachment hashes.
  • Code Cells (code): Contain executable programming language source lines, an execution counter integer (execution_count) reflecting kernel execution sequence, and an ordered list of output objects (outputs).
  • Raw Cells (raw): Contain unformatted text intended to pass unaltered into specific export targets (such as raw LaTeX macro calls targeted strictly at TeX compilers).

The Multiline String Representation Problem

One of the most persistent operational hurdles when parsing Jupyter notebooks across heterogeneous ecosystems is the string serialization format. In the official schema, the source field is defined as:

source∈{string,Array⟨string⟩}\text{source} \in \{\text{string}, \text{Array}\langle\text{string}\rangle\}

Jupyter web applications, VS Code, and Google Colaboratory frequently serialize multi-line code buffers not as a single newline-delimited string, but as an array of individual line strings where each element preserves its trailing newline character:

{
  "cell_type": "code",
  "execution_count": 4,
  "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "data = np.random.normal(0, 1, 1000)\n",
    "plt.hist(data, bins=30)\n"
  ],
  "outputs": []
}

Parsers that assume source is always a primitive string crash immediately when encountering an array. A robust AST normalizer must normalize this value into a unified string representation during ingestion:

// AST normalizer ensuring source lines are reliably flattened
export function normalizeSource(source: string | string[]): string {
  if (Array.isArray(source)) {
    return source.join('');
  }
  return source ?? '';
}

3. The MIME Bundle Ecosystem in Cell Outputs

The most sophisticated component of the Jupyter specification is the cell output subsystem. Code cells do not merely capture raw terminal text. They capture rich multi-modal computational artifacts structured as MIME bundles.

+-----------------------------------------------------------------------+
|                    Output MIME Bundle Architecture                    |
+-----------------------------------------------------------------------+
|  CodeCell.outputs[]                                                   |
|       |                                                               |
|       +--> stream: { name: "stdout"|"stderr", text: string|string[] } |
|       |                                                               |
|       +--> error:  { ename: string, evalue: string, traceback: [] }   |
|       |                                                               |
|       +--> execute_result / display_data                              |
|            {                                                          |
|              "execution_count": 4,                                    |
|              "data": {                                                |
|                "text/plain": "<Figure size 640x480 with 1 Axes>",    |
|                "image/png": "iVBORw0KGgoAAAANSUhEUgAA...",           |
|                "text/html": "<div>...</div>"                          |
|              },                                                       |
|              "metadata": {}                                           |
|            }                                                          |
+-----------------------------------------------------------------------+

The Four Fundamental Output Variants

  1. stream: Captures standard output (stdout) and standard error (stderr) console buffers emitted by print statements or runtime logs.
  2. execute_result: The value returned by evaluating the final expression in a code cell, carrying an execution_count integer.
  3. display_data: Rich visual media generated explicitly via display utilities (e.g., IPython.display.display()), carrying no execution counter.
  4. error: Uncaught runtime exceptions, containing the exception name (ename), exception value (evalue), and a traceback array containing terminal color codes.

The MIME Selection Priority Matrix

Both execute_result and display_data store their payloads inside a data dictionary where keys represent standard Internet Media Types (MIME types). This design allows a single computation to provide multiple representations of differing fidelity simultaneously.

When rendering a notebook cell output, the renderer must traverse a deterministic priority ladder, selecting the highest-fidelity format supported by the rendering target:

// Resolution priority for web rendering contexts
const MIME_SELECTION_PRIORITY: readonly string[] = [
  'application/vnd.jupyter.widget-view+json',
  'image/svg+xml',
  'image/png',
  'image/jpeg',
  'text/html',
  'text/markdown',
  'text/latex',
  'text/plain'
];

export function resolveOptimalMime(data: Record<string, unknown>): { mimeType: string; payload: unknown } | null {
  for (const mimeType of MIME_SELECTION_PRIORITY) {
    if (mimeType in data) {
      return { mimeType, payload: data[mimeType] };
    }
  }
  return null;
}

If an interactive charting library emits an SVG vector diagram along with a fallback PNG and a plain-text label, a web renderer chooses image/svg+xml for crisp vector scaling, whereas an export pipeline targeting a plain terminal chooses text/plain.

Parsing Terminal ANSI Escape Sequences

Error tracebacks and streaming console logs frequently contain raw ANSI terminal escape sequences used for colorization and cursor positioning (for instance, \u001b[0;31m representing red text).

Displaying raw ANSI codes in web user interfaces produces unreadable escape noise. Parsers must run an ANSI tokenization pass to convert terminal escape sequences into structured HTML spans with appropriate styling:

// Lightweight ANSI sequence tokenizer for browser safe log display
export function parseAnsiToHtml(rawAnsi: string): string {
  const ANSI_COLOR_MAP: Record<number, string> = {
    30: 'var(--term-black, #000)',
    31: 'var(--term-red, #e06c75)',
    32: 'var(--term-green, #98c379)',
    33: 'var(--term-yellow, #e5c07b)',
    34: 'var(--term-blue, #61afef)',
    35: 'var(--term-magenta, #c678dd)',
    36: 'var(--term-cyan, #56b6c2)',
    37: 'var(--term-white, #abb2bf)'
  };

  const tokenRegex = /\u001b\[([0-9;]+)m([^\u001b]*)/g;
  let html = '';
  let lastIndex = 0;
  let match: RegExpExecArray | null;

  while ((match = tokenRegex.exec(rawAnsi)) !== null) {
    const codes = match[1].split(';').map(Number);
    const content = match[2];
    
    let colorStyle = '';
    for (const code of codes) {
      if (ANSI_COLOR_MAP[code]) {
        colorStyle = `color: ${ANSI_COLOR_MAP[code]};`;
      }
    }

    html += `<span style="${colorStyle}">${escapeHtml(content)}</span>`;
    lastIndex = tokenRegex.lastIndex;
  }

  return html || escapeHtml(rawAnsi);
}

function escapeHtml(text: string): string {
  return text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');
}

4. Security Architecture: XSS Prevention and Sandbox Sanitization

Rendering arbitrary .ipynb documents inside a web browser presents significant security risks. Because Jupyter notebooks support embedded HTML outputs (text/html) and scalable vector graphics (image/svg+xml), a notebook can act as a carrier for stored Cross-Site Scripting (XSS) attacks.

+-----------------------------------------------------------------------+
|                    XSS Attack Vector Attack Surface                   |
+-----------------------------------------------------------------------+
|  Malicious Notebook (.ipynb)                                          |
|       |                                                               |
|       v                                                               |
|  [ Cell Output: text/html ] ---> <img src="x" onerror="evil()" />     |
|  [ Cell Output: image/svg ] ---> <svg><script>stealToken()</script>   |
|  [ Markdown: raw html ] -------> <iframe src="phishing.com"></iframe> |
|       |                                                               |
|       v                                                               |
|  Unsanitized Browser Rendering Engine                                 |
|       |                                                               |
|       X--> CRITICAL VULNERABILITY: Session Hijacking, Token Theft     |
+-----------------------------------------------------------------------+

The Vector Attack Surface

  1. Inline HTML Injection: A notebook script can print raw <script> tags, malicious <iframe> elements, or image elements configured with inline event handlers (<img src=x onerror=alert(document.cookie)>).
  2. SVG Embedded Scripting: The SVG specification allows executable <script> blocks inside vector graphics. When an SVG payload is placed into the DOM via innerHTML, malicious scripts run within the host origin.
  3. Markdown Tag Smuggling: Markdown allows raw HTML blocks, meaning a markdown cell can hide invisible tracking pixels or keyloggers.

Enforcing Strict DOM Sanitization

To mitigate these attack vectors, every HTML, SVG, and Markdown string must pass through a strict sanitization pipeline before reaching the DOM. Using DOMPurify with an aggressive allowlist ensures that only safe layout tags and presentation attributes are preserved:

import DOMPurify from 'dompurify';

export class NotebookSecuritySanitizer {
  private static purifyConfig = {
    ALLOWED_TAGS: [
      'b', 'i', 'em', 'strong', 'a', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
      'ul', 'ol', 'li', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'code',
      'pre', 'div', 'span', 'img', 'svg', 'path', 'g', 'circle', 'rect', 'line'
    ],
    ALLOWED_ATTR: [
      'href', 'target', 'src', 'alt', 'title', 'class', 'style',
      'width', 'height', 'viewBox', 'd', 'fill', 'stroke'
    ],
    FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form'],
    FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover'],
    RETURN_DOM_FRAGMENT: false,
    RETURN_DOM: false
  };

  public static sanitizeHtmlOutput(rawHtml: string): string {
    return DOMPurify.sanitize(rawHtml, this.purifyConfig);
  }
}

Mathematical expressions (...... and ......) must be routed to KaTeX or MathJax parsers rather than evaluated via generic string substitution. This ensures that math syntax is converted safely into standard DOM spans without executing arbitrary JavaScript.

5. High-Fidelity Local Document Conversion Pipelines

The PetexSpace convert Jupyter notebooks in the browser utility uses this normalized AST pipeline to deliver instant, secure conversions across document formats. Users can also transform document formats locally without transmitting computational research files across the internet.

+-----------------------------------------------------------------------+
|               PetexSpace Jupyter Conversion Architecture              |
+-----------------------------------------------------------------------+
|  User Ingest (.ipynb file)                                            |
|       |                                                               |
|       v                                                               |
|  [ JSON Schema Validator (nbformat v4 Invariants) ]                   |
|       |                                                               |
|       v                                                               |
|  [ AST Normalization Engine ]                                         |
|    - String array flattening                                          |
|    - ANSI escape code strip & tokenization                            |
|    - MIME bundle priority sorting                                     |
|    - DOMPurify XSS scrubbing                                          |
|       |                                                               |
|       +-----------------------+-----------------------+               |
|       |                       |                       |               |
|       v                       v                       v               |
|  [ Markdown Emitter ]   [ LaTeX Emitter ]       [ PDF Pipeline ]      |
|  - GFM Tables           - Booktabs formatting   - Vector layout       |
|  - MathJax blocks       - Listing code fences   - High-DPI graphics   |
|  - Clean image refs     - Figure placement      - Paginated output    |
+-----------------------------------------------------------------------+

AST Construction and Target Emission

Once a notebook is parsed and sanitized, it is transformed into a clean internal Abstract Syntax Tree (AST):

export interface NotebookAST {
  language: string;
  kernelName: string;
  nodes: ASTNode[];
}

export type ASTNode = 
  | { type: 'heading'; level: number; content: string }
  | { type: 'prose'; htmlContent: string }
  | { type: 'code_block'; language: string; source: string; executionCount: number | null }
  | { type: 'figure'; format: 'png' | 'svg'; base64Data: string; caption?: string }
  | { type: 'table'; htmlContent: string }
  | { type: 'console_output'; text: string; isError: boolean };

From this normalized AST, generating target formats becomes deterministic:

  • Markdown Generation: Code cells are enclosed in fenced code blocks (code blocks). Images are converted from base64 strings into inline data URIs or companion assets, and Markdown cells are passed with normalized line endings.
  • Academic LaTeX Generation: Code cells are wrapped inside listings or minted environments. Rich tabular data is transformed from HTML <table> elements into professional booktabs matrices, and equations are mapped to native equation environments.
  • Vector-Accurate PDF Generation: The AST streams directly into an in-memory layout engine that calculates page breaks, embeds TrueType font subsets, and places high-resolution figures without browser print dialog distortions.

Privacy and Operational Performance

Processing computational research documents locally eliminates severe enterprise security risks. Scientific notebooks often contain proprietary algorithms, unredacted corporate database queries, patient healthcare metrics, or confidential financial models.

By executing all parsing, validation, sanitization, and format transformations directly inside the user's browser runtime, PetexSpace guarantees that private data never leaves the host machine.

Client-side execution eliminates network transfer latency, ensuring that multi-megabyte machine learning notebooks containing hundreds of embedded plots convert instantaneously.