Convert ChatGPT JSON to PDF, Word or Markdown (2026)
When you export your data from ChatGPT, you get a \conversations.json\ file. This file contains every conversation you've ever had, but it's structured in a way that's nearly impossible to read without tools. Let's break down exactly what's in this file and how to convert it. The fast answer If you just want readable f…
When you export your data from ChatGPT, you get a \conversations.json\ file. This file contains every conversation you've ever had, but it's structured in a way that's nearly impossible to read without tools. Let's break down exactly what's in this file and how to convert it.
The fast answer
If you just want readable files:
Open the ChatExports converter and drop in your \conversations.json\ (or the whole export ZIP).
Browse or search your conversations and pick the ones you need.
Download them as Word (.docx), PDF, or Markdown — single chats or the whole archive at once.
Everything runs in your browser tab, so the export never leaves your computer. The same flow works for Claude, Gemini, Perplexity, Poe, Grok, Copilot, NotebookLM, and Google AI Studio exports.
Just want to read it? Use a ChatGPT JSON viewer
If you don't need files yet, open the ChatGPT JSON viewer. It reconstructs each thread in the order you actually had it, lists every chat by title and date, and lets you search across all of them — no export, no upload, no account.
Which format should you pick?
Word (.docx) — best when you need to edit, comment, or share with colleagues.
PDF — best for archives, records, and anything that must look identical everywhere (full Unicode and emoji included). See the step-by-step export ChatGPT conversation to PDF guide.
Markdown (.md) — best for Obsidian, Notion, or a Git-tracked knowledge base.
The rest of this guide explains what is actually inside the JSON, in case you want to parse it yourself.
Understanding the ChatGPT JSON Structure
The \conversations.json\ file is an array of conversation objects. Each conversation contains:
title: the conversation title you see in ChatGPT's sidebar
create_time / update_time: Unix timestamps (seconds since 1970)
mapping: a tree of message nodes, not a simple list
moderation_results: content filtering data
conversation_id: a unique identifier
Why It's a Tree, Not a List
ChatGPT conversations aren't simple back-and-forth exchanges. They can have:
Branching: when you edit a message and regenerate, it creates a new branch
System messages: invisible prompts that set ChatGPT's behavior
Multiple content parts: a single message can contain text, images, and code
This means the messages are stored as a tree graph with parent-child relationships, not a flat list. To reconstruct the conversation in order, you need to:
Find the root node (the one with no parent)
Follow the children chain from root to leaves
Choose the correct branch at each fork
Filter out system messages and empty nodes
Handle special content types (images, code blocks, tool results)
Field-by-field reference
Every entry in \mapping\ is a node with this shape:
The root node has \message: null\. It exists only to anchor the tree, so skip it instead of crashing on it.
\parent\ can point at a node that is not in the mapping on partial or trimmed exports. Treat "parent missing" as a second root condition.
\weight: 0\ marks a message the UI has deprioritized (usually a superseded branch). Most people want to drop these.
\recipient\ is \"all"\ for normal replies. Anything else (\"python"\, \"browser"\, a tool name) means the message is an internal tool call, not chat text.
\metadata.is_visually_hidden_from_conversation: true\ marks messages ChatGPT never displayed — custom-instruction injections, safety scaffolding, memory writes. Include them and your document fills with text you never saw.
content_type values you will actually hit
\content.parts\ is only present for plain text. Other content types put their payload in different keys, which is the single most common reason home-made scripts silently produce empty documents:
| \content_type\ | Where the text lives | Keep it? |
|---|---|---|
| \text\ | \parts[]\ (strings) | Yes |
| \multimodal_text\ | \parts[]\ of mixed strings and objects (\{"asset_pointer": ...}\ for images) | Text parts yes, image pointers are dead links |
| \code\ | \content.text\ | Usually yes, as a code block |
| \execution_output\ | \content.text\ | Yes, if you want Code Interpreter results |
| \tether_browsing_display\ / \tether_quote\ | \content.result\ / \content.text\ | Usually no |
| \user_editable_context\ | \content.user_profile\ / \user_instructions\ | No — these are your custom instructions |
| \model_editable_context\ | \content.model_set_context\ | No — memory entries |
So a correct text extractor is not \parts[0]\. It is: check \parts\ (and flatten objects inside it), then \text\, then \result\, then \thoughts\, then give up and return an empty string.
Branching: picking the right leaf
When you edit a prompt and regenerate, ChatGPT forks the tree. A node's \children\ array holds every version in creation order, so the last child is the most recent branch — that is what the ChatGPT UI shows you by default. Walking \children[0]\ gives you the abandoned first drafts instead.
Two failure modes to guard against:
Cycles. Corrupted exports occasionally contain a node that is its own ancestor. Keep a \visited\ set or your walk hangs.
Detached subtrees. Some accounts (especially after archive/restore) export conversations where the main thread is empty. The reliable fallback is to ignore the tree entirely, collect every node with a valid message, and sort by \create_time\.
Timestamps and ordering
\create_time\ and \update_time\ are float Unix seconds, not milliseconds — multiply by 1000 before \new Date()\, or you get 1970. They are also nullable on system-generated nodes, so never sort on them without a fallback. The top-level \update_time\ is what ChatGPT's sidebar sorts by, which is why conversation order in your export rarely matches creation order.
The format has changed more than once
Exports from 2023 use a flat \messages\ array on some conversations. Exports from mid-2025 onward added \thoughts\ content for reasoning models and moved some attachments to \metadata.attachments\. A parser written against a single export usually breaks on the next one — which is why the answer to "why does my old script return no conversations?" is nearly always a new \content_type\.
The Manual Approach (Not Recommended)
You *could* write a script to parse the JSON yourself. Here's a simplified example:
\\\`python
import json
with open('conversations.json', 'r') as f:
conversations = json.load(f)
for conv in conversations:
print(f"=== {conv['title']} ===")
mapping = conv['mapping']
Find the root node
root = None
for node_id, node in mapping.items():
if node.get('parent') is None:
root = node_id
break
Walk the tree
current = root
while current:
node = mapping[current]
msg = node.get('message')
if msg and msg.get('content', {}).get('parts'):
role = msg['author']['role']
text = msg['content']['parts'][0]
if isinstance(text, str) and text.strip():
print(f"[{role}]: {text[:100]}...")
children = node.get('children', [])
current = children[-1] if children else None
\\\`
This basic script misses a lot:
It doesn't handle branching properly
It ignores images, code interpreter results, and file uploads
It doesn't preserve formatting (bold, lists, code blocks)
Upload your \conversations.json\ or the full ZIP file
Preview each conversation with proper formatting
Export to Word, PDF, or Markdown. individually or in bulk
What ChatExports Handles That Scripts Don't
✅ Proper tree traversal with branch selection
✅ Formatted code blocks with syntax highlighting
✅ Bold, italic, lists, and headings preserved
✅ System messages filtered out
✅ Empty or broken messages skipped
✅ Timestamps converted to readable dates
✅ Bulk export with ZIP download
✅ Works entirely in your browser (nothing uploaded)
Claude and Gemini Exports Are Different
If you also use Claude or Gemini, their export formats are completely different:
Claude exports a \conversations.json\ but with a flat message array (simpler than ChatGPT's tree structure)
Gemini (via Google Takeout) exports individual HTML or JSON files per conversation
ChatExports handles all three formats automatically. Upload any of them and it detects the format.
FAQ
Can I just use the chat.html file in the ZIP?
OpenAI includes a basic HTML viewer, but it's extremely limited. It doesn't support search, export, or any formatting. Most people find it barely usable.
Will this work with ChatGPT Plus, Teams, or Enterprise exports?
Yes. The JSON format is the same across all ChatGPT plans.
How big can the JSON file be?
ChatExports processes everything in your browser, so it depends on your device's memory. Most exports (even with thousands of conversations) work fine on modern devices.
Why does my export show "no conversations found"?
Almost always a format change: a \content_type\ your script does not know about, or a conversation using a flat \messages\ array instead of \mapping\. A parser that falls back through \parts\ → \text\ → \result\ → \thoughts\ handles both.
Can I get one row per message instead of a document?
Yes — that is a CSV export. Each message becomes a row with conversation title, role, timestamp, and text, which is what you want for analysis in Excel, Sheets, or pandas.
What about images and file attachments?
Images generated by DALL-E are referenced by URL in the JSON. Text content from Code Interpreter and file uploads is included inline. ChatExports preserves the text content; image URLs may expire over time.