Everywhere except the hot path
Game engines are famously performance-obsessed, which makes it easy to assume JSON has no place in one — and inside the actual render/physics loop, that's usually correct. But nearly everything *around* that loop — save files, level data, dialogue trees, config, mod support — is exactly the kind of infrequently-read, human-editable, tooling-friendly data JSON is good at, which is why it shows up constantly in game codebases despite the performance reputation.
Save files
A save file is read once at load and written once on save — not a hot path — which makes JSON's overhead irrelevant and its debuggability genuinely valuable: a corrupted or unexpected save state is far easier to diagnose by opening the file in a text editor than by attaching a hex viewer to a binary blob.
{
"player": {
"level": 14,
"health": 82,
"inventory": [
{ "itemId": "sword_iron", "quantity": 1 },
{ "itemId": "potion_health", "quantity": 3 }
],
"position": { "x": 412.5, "y": -18.2, "map": "forest_01" }
},
"flags": { "metWizard": true, "bridgeRepaired": false }
}Unity's JsonUtility and Newtonsoft.Json (via the Json.NET package) both serialize a C# save-data class directly to and from this shape; Godot's built-in JSON singleton does the same for GDScript. Versioning matters more here than almost anywhere else — add a "saveVersion": 3 field from day one, since a save format that outlives several game updates will eventually need to migrate old saves forward, and a version field is what makes that migration logic possible instead of guessing from the shape of the data itself.
Level and tilemap data
Tiled, the most widely used 2D level editor, exports maps as either its own XML format or JSON — and the JSON export has become the de facto standard for custom engine integrations precisely because it's trivial to parse in any language without a dedicated XML library:
{
"width": 20,
"height": 15,
"tilewidth": 32,
"layers": [
{ "name": "ground", "data": [1, 1, 1, 2, 2, 1, 1, 3, ...] }
]
}The data array here is a flat list of tile IDs, one per grid cell — simple enough that a custom engine can write its own 20-line parser rather than depending on a full Tiled-format library, which is common for smaller or performance-sensitive engines that want tight control over how tile data loads.
Dialogue trees and narrative data
Branching dialogue is naturally tree/graph-shaped data, and writers on a team are rarely the same people writing engine code — a JSON (or JSON-adjacent, like Ink's compiled JSON output) dialogue format lets narrative designers iterate without touching a build:
{
"id": "wizard_intro",
"text": "You've come far, traveler.",
"choices": [
{ "text": "Who are you?", "next": "wizard_backstory" },
{ "text": "I need your help.", "next": "wizard_quest_offer" }
]
}Several dialogue tools (Yarn Spinner, Ink) compile their own authoring syntax down to JSON specifically so the runtime-side parsing logic in the engine stays simple and language-agnostic, while writers work in a friendlier custom syntax upstream of that compilation step.
Where JSON is the wrong choice
Anything in the render or physics loop. Parsing text every frame is wasted CPU that a binary format (or better, data already loaded into typed structs/arrays in memory) avoids entirely — JSON has no place inside a loop that runs 60+ times a second.
Large asset payloads — meshes, textures, audio. These are inherently binary data; encoding a texture as a JSON array of pixel values would be enormously larger and slower to parse than the binary image formats built for exactly this.
Anti-cheat-sensitive save data. A save file a player can trivially open in a text editor and hand-edit ("gold": 999999) is a real problem for any game with online leaderboards or competitive elements — some studios encrypt or checksum JSON save files specifically to detect tampering, or move to a binary format with less casual editability as a deterrent (not a real security boundary, just raising the bar past "open in Notepad").