Understanding the ChatGPT conversations.json Format

If you are writing your own parser for a ChatGPT export, this is the reference I wish I had had. It covers the structure of \conversations.json\, how to reconstruct a transcript correctly, and the edge cases that make naive parsers fail quietly. What is in the export Request your data from ChatGPT under Settings, Data …

If you are writing your own parser for a ChatGPT export, this is the reference I wish I had had. It covers the structure of \conversations.json\, how to reconstruct a transcript correctly, and the edge cases that make naive parsers fail quietly.

What is in the export

Request your data from ChatGPT under Settings, Data controls, Export data. OpenAI emails a ZIP within a few minutes. Inside, the file that matters is \conversations.json\. It is an array, one entry per conversation, each with a \title\, a \create_time\, a \current_node\, and a \mapping\.

The mapping is a tree, not a list

This is the single most important thing. \mapping\ is an object keyed by node ID. Each node looks roughly like:

\\\`json

{

"id": "abc-123",

"message": { "author": { "role": "assistant" }, "content": { ... }, "create_time": 1750000000 },

"parent": "def-456",

"children": ["ghi-789"]

}

\\\`

Every time you edit a prompt or regenerate a response, ChatGPT creates a new branch rather than replacing anything. The conversation you actually saw is one path through that tree.

Reconstructing the visible transcript

Start at \current_node\, follow \parent\ references until you reach a node with no parent, collecting nodes as you go, then reverse the list. That path is the conversation as displayed. Everything else in the mapping is an abandoned branch.

If you sort the mapping's values by timestamp instead, you get all branches interleaved. This is the most common bug in DIY parsers, and it produces output that looks fine at a glance.

Content types you have to handle

The \content\ object varies by \content_type\:

Also check \author.role\. Beyond \user\ and \assistant\ you will see \system\ and \tool\. And watch for \message.metadata.is_visually_hidden_from_conversation\, which marks nodes that were never shown.

Nodes with no message

The root node of every mapping has \"message": null\. So do some structural nodes. Any code that reads \node.message.author\ without a null check will crash on the first conversation.

Timestamps

\create_time\ and \update_time\ are Unix seconds as floats, and can be null on some nodes. If you sort or format them without a null guard, you will get \Invalid Date\ in filenames and headers.

The rest of the ZIP

Alongside \conversations.json\ you get \chat.html\, a single self-contained page with the same data embedded, plus \message_feedback.json\, \model_comparisons.json\, \user.json\, and a folder of uploaded images and DALL-E outputs referenced by the multimodal parts. If you want images in your output, you need to resolve those file pointers to paths in the ZIP.

Turning it into documents

Parsing is the first half. The second half is rendering. Message text is Markdown, so producing a decent Word file means parsing the Markdown and mapping it to docx styles: heading levels, monospace runs with a background for code, list numbering at multiple depths. PDFs need an embedded Unicode font or emoji and accented characters silently disappear.

We wrote about that side of it in what it really costs to build your own export script, and about the specific bugs AI-generated parsers ship with in vibe coding an AI chat exporter.

If you would rather not

ChatExports handles all of the above for ChatGPT plus eight other platforms, and runs entirely in your browser so the file never leaves your machine. $10 for one platform, $25 for all of them, one time. If you want the build-versus-buy numbers laid out, they are on this page.