JSON Inside JSON: The Developer's Guide to Escaping and Unescaping

A dev's guide to JSON escaping and unescaping. Learn why nested JSON breaks, see code examples in JS, Python, Java & Go, and avoid common pitfalls.

By Belqassim Djafer · Published · Updated

A common source of bugs in data pipelines is embedding a JSON object as a string value within another JSON object. A service fails to parse a payload, and the error is a cryptic SyntaxError: Unexpected token. The payload looks like valid JSON, the service that sent it seems to be creating valid JSON, yet the parser fails.

The issue is almost always improper JSON escaping. This happens when a string value contains characters that have special meaning in JSON syntax, like double quotes (") or backslashes (\). To include these characters literally inside a string, they must be escaped. Let's get this right.

Why JSON Escaping is Necessary

The JSON specification, RFC 8259, defines a string as a sequence of characters enclosed in double quotes. This creates a problem when the string itself needs to contain a double quote.

This is invalid JSON because the parser sees the quote after said, and considers the string finished. The Hello that follows is a syntax error.

{
  "message": "He said, "Hello, world!""
} 
// INVALID JSON

To solve this, the spec defines escape sequences. A backslash (\) signals that the next character should be treated as a literal part of the string, not as syntax.

The primary escape sequences are:

  • \" → " (a literal double quote)
  • \\ → \ (a literal backslash)
  • \/ → / (a literal forward slash; escaping is optional but common)
  • \b → backspace
  • \f → form feed
  • \ → newline
  • \r → carriage return
  • \ → tab

Applying this, the invalid example becomes valid:

{
  "message": "He said, \"Hello, world!\""
}
// VALID JSON

So what do I do with this? When you need to embed a complete JSON object as a string value inside another JSON object, you must convert that inner object into a string and escape all its special characters. This process is called "stringifying." All standard JSON libraries do this for you. Your job is to call the right function.

How to Escape and Unescape Programmatically

You should never attempt to escape JSON by hand using string replacement. It's error-prone and will miss edge cases. Always use your language's standard JSON library.

The pattern is always the same:

  1. To Embed (Escape): Take the inner object and serialize (stringify) it into a JSON string. This string is now a safe, escaped value you can place into the outer object. Then, serialize the outer object.
  2. To Extract (Unescape): Parse the outer JSON. The inner data will be a string. Access that string value and parse it again to get the original inner object.

JavaScript / TypeScript

Use JSON.stringify() to escape and JSON.parse() to unescape.

// Inner object representing some configuration
const widgetConfig = {
  type: 'chart',
  datasetId: 'ds-987',
  options: {
    color: '#FF5733',
    is3D: false
  }
};

// 1. Stringify the inner object to escape it.
const stringifiedConfig = JSON.stringify(widgetConfig);

// 2. Create the outer payload with the escaped string.
const dashboardPayload = {
  dashboardId: 'dash-123',
  widgetName: 'Sales Q4',
  // The 'config' field is required to be a string.
  config: stringifiedConfig
};

// 3. Serialize the final payload to send over the network.
const finalJson = JSON.stringify(dashboardPayload, null, 2);

console.log(finalJson);
/* Output:
{
  "dashboardId": "dash-123",
  "widgetName": "Sales Q4",
  "config": "{\"type\":\"chart\",\"datasetId\":\"ds-987\",\"options\":{\"color\":\"#FF5733\",\"is3D\":false}}"
}
*/

// --- On the receiving end ---

// 4. Parse the outer payload.
const receivedPayload = JSON.parse(finalJson);

// The 'config' field is just a string at this point.
console.log(typeof receivedPayload.config); // 'string'

// 5. Parse the inner JSON string to get the object.
const parsedConfig = JSON.parse(receivedPayload.config);

console.log(parsedConfig.options.color); // '#FF5733'

Python

The json module provides json.dumps() (dump to string) for escaping and json.loads() (load from string) for unescaping.

import json

# Inner data (as a Python dict)
event_details = {
    'ip_address': '203.0.113.42',
    'user_agent': 'Mozilla/5.0',
    'tags': ['login_attempt', 'success']
}

# 1. Stringify to escape.
stringified_details = json.dumps(event_details)

# 2. Create the main log entry.
log_entry = {
    'timestamp': '2023-10-27T10:00:00Z',
    'level': 'INFO',
    # This field must be a string for the logging system.
    'details': stringified_details
}

# 3. Serialize the final log entry.
final_json_log = json.dumps(log_entry, indent=2)
print(final_json_log)

# --- On the receiving end (e.g., a log analysis script) ---

# 4. Parse the outer log entry.
received_log = json.loads(final_json_log)

# 5. Parse the inner details string.
parsed_details = json.loads(received_log['details'])

print(parsed_details['ip_address']) # '203.0.113.42'

Java (with Jackson)

In Java, libraries like Jackson or Gson are standard. The principle of serializing the inner object to a string before embedding it remains the same.

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.IOException;

public class JsonEscapeExample {
    public static void main(String[] args) throws IOException {
        ObjectMapper mapper = new ObjectMapper();

        // Inner JSON object
        ObjectNode actionPayload = mapper.createObjectNode();
        actionPayload.put("action", "update_status");
        actionPayload.put("new_status", "completed");

        // 1. Stringify the inner object.
        String stringifiedPayload = mapper.writeValueAsString(actionPayload);

        // 2. Create the outer message.
        ObjectNode message = mapper.createObjectNode();
        message.put("messageId", "msg-xyz-789");
        message.put("queue", "actions_queue");
        message.put("payload", stringifiedPayload);

        // 3. Serialize the final message.
        String finalJson = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(message);
        System.out.println(finalJson);

        // --- On the receiving end ---

        // 4. Parse the outer message.
        JsonNode receivedMessage = mapper.readTree(finalJson);
        String innerPayloadStr = receivedMessage.get("payload").asText();

        // 5. Parse the inner JSON string.
        JsonNode parsedInnerPayload = mapper.readTree(innerPayloadStr);
        System.out.println(parsedInnerPayload.get("new_status").asText()); // "completed"
    }
}

Go

The encoding/json package is the standard library for this. json.Marshal and json.Unmarshal are the key functions.

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

// Outer struct
type Message struct {
	ID       string `json:"id"`
	Metadata string `json:"metadata"` // Field must be a string
}

// Inner struct
type Metadata struct {
	Source    string `json:"source"`
	TraceID   string `json:"traceId"`
}

func main() {
	// Inner data
	innerData := Metadata{
		Source:    "service-api",
		TraceID:   "trace-abc-123",
	}

	// 1. Marshal the inner struct to an escaped JSON byte slice.
	innerJSON, err := json.Marshal(innerData)
	if err != nil {
		log.Fatal(err)
	}

	// 2. Create the outer message with the inner JSON as a string.
	outerMessage := Message{
		ID:       "evt-456",
		Metadata: string(innerJSON),
	}

	// 3. Marshal the final message.
	finalJSON, err := json.MarshalIndent(outerMessage, "", "  ")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(finalJSON))

	// --- On the receiving end ---

	// 4. Unmarshal the outer message.
	var receivedMessage Message
	if err := json.Unmarshal(finalJSON, &receivedMessage); err != nil {
		log.Fatal(err)
	}

	// 5. Unmarshal the inner metadata string.
	var parsedMetadata Metadata
	if err := json.Unmarshal([]byte(receivedMessage.Metadata), &parsedMetadata); err != nil {
		log.Fatal(err)
	}

	fmt.Println(parsedMetadata.TraceID) // "trace-abc-123"
}

Real-world Usage: When is This Necessary?

If you control both the producer and consumer, a standard nested JSON object is almost always better than stringifying a field. However, this pattern is often a requirement when interfacing with systems you don't control.

1. Storing Unstructured Data in a Relational Database

A common scenario is having a flexible metadata column in a SQL table. The column is usually of type TEXT, VARCHAR, or JSON. Even with a native JSON type, some ORMs or database drivers may require you to provide a string.

For example, an events table might have a details column that stores the raw JSON payload from an external webhook. To store this payload without breaking the structure of the event record itself, it's stringified and saved.

2. Message Queues and Streaming Systems

Systems like AWS SQS, RabbitMQ, or Apache Kafka are designed to transport messages. Often, the message body itself must be a string. If you're sending a JSON object, you stringify it. But sometimes the message envelope also has its own metadata fields, and one of those fields might need to contain your JSON object.

A typical example is a log aggregator that expects log entries as JSON objects. One field in the log might be original_request, which contains the full JSON body of an incoming HTTP request. That body must be escaped to fit inside the log entry's JSON structure.

Common Mistakes and Pitfalls

1. Forgetting the Second Parse

This is the most frequent error. After parsing the outer JSON, developers forget that the inner field is still a string. They try to access its properties directly, which fails.

const payload = '{"id": 1, "data": "{\"value\": 42}"}';
const parsed = JSON.parse(payload);

console.log(typeof parsed.data); // 'string'

// This will not work!
// parsed.data.value is undefined because 'parsed.data' is a string.
console.log(parsed.data.value); // undefined

// Correct way:
const innerData = JSON.parse(parsed.data);
console.log(innerData.value); // 42

2. Unnecessary Escaping (Nested Object vs. String)

Don't use this pattern if you don't have to. If the schema for your payload allows a nested object, use that. It's cleaner, more efficient, and less error-prone.

// PREFER THIS (nested object) if possible:
{
  "id": 123,
  "metadata": {
    "source": "api",
    "version": 2
  }
}

// ONLY USE THIS (escaped string) if the 'metadata' field MUST be a string:
{
  "id": 123,
  "metadata": "{\"source\":\"api\",\"version\":2}"
}

3. Double or Triple Escaping

In complex data pipelines, a payload might pass through multiple systems, and each one might mistakenly stringify an already-stringified field. This results in a string with layers of escaped backslashes (\\\") that becomes very difficult to debug.

If you see \" or \\ in your final JSON, it's a sign that JSON.stringify() (or its equivalent) was called one too many times. The fix is to find the redundant serialization step in the pipeline and remove it.

When debugging, it can be helpful to paste the problematic string into an online tool like a JSON Escape / Unescape to manually unwrap the layers and see the underlying object.

4. Manual String Manipulation

Never, ever try to build or escape JSON using string concatenation or replacement. It's a guaranteed path to bugs.

// DO NOT DO THIS
const value = 'some "quoted" text';
const badJson = '{"key": "' + value.replace(/"/g, '\\"') + '"}'; // Wrong and brittle

// ALWAYS DO THIS
const goodJson = JSON.stringify({ key: value });

Standard libraries correctly handle all edge cases, including escaping backslashes themselves, newlines, tabs, and other control characters.

Conclusion

Embedding JSON within JSON is a straightforward process once you recognize the pattern. It's not a hack, but a consequence of JSON's strict syntax for string values.

The rules are simple:

  1. When sending: Use your language's standard library to stringify the inner object into an escaped string before placing it in the outer object.
  2. When receiving: Parse the outer object first, then take the resulting string value and parse it a second time to reconstruct the inner object.
  3. When designing: Prefer native nested objects unless an external system forces you to use a string field.

Stick to these rules, and you'll handle nested JSON correctly and avoid a whole class of frustrating parsing errors.

Related Tools