[{"data":1,"prerenderedAt":187},["ShallowReactive",2],{"article-nested-json-escaping":3},{"article":4,"relatedArticles":139},{"slug":5,"category":6,"date":7,"readingMinutes":8,"tags":9,"relatedTool":15,"cn":16,"en":73,"author":127,"reviewedBy":130,"reviewedDate":132,"updatedDate":7,"sources":133,"noIndex":138},"nested-json-escaping","踩坑避坑","2026-07-28",9,[10,11,12,13,14],"JSON","转义","反斜杠","JSON.stringify","嵌套 JSON","/tools/json/json-compress-escape",{"title":17,"description":18,"intro":19,"sections":20},"嵌套 JSON 反斜杠为什么变多：分层转义与解析","解释对象、JSON 文本、嵌套 JSON 字符串和 JavaScript 源码四个表示层为什么产生不同数量的反斜杠，并给出逐层 stringify、parse 与类型判断方案。","日志或接口中出现 `{\"payload\":\"{\\\"name\\\":\\\"Tiny\\\"}\"}`，通常不是“反斜杠失效”，而是 JSON 文本又被放进了另一个 JSON 字符串。每跨过一层字符串语法，双引号和反斜杠都必须再次转义。本文只处理嵌套 JSON 的表示层问题，不讨论普通 JSON 语法错误或 Unicode `\\uXXXX` 的编码原理。",[21,27,33,39,45,50,60,66],{"heading":22,"paragraphs":23,"code":26},"一、先区分值、JSON 文本和源码字面量",[24,25],"同一份数据至少可能以三种形态出现：内存中的对象、JSON.stringify 返回的文本、写在 JavaScript 源码中的字符串字面量。开发者看到的斜杠数量取决于当前观察的是哪一层。","控制台和调试器为了展示字符串边界，可能使用带转义的预览。判断真实内容时，应同时查看 typeof、字符串长度以及 console.log 的直接输出，不要只看对象预览中的引号。","const value = { message: 'He said \"hello\"', path: 'C:\\\\temp\\\\a.txt' };\nconst jsonText = JSON.stringify(value);\n\nconsole.log(typeof value);    // \"object\"\nconsole.log(typeof jsonText); // \"string\"\nconsole.log(jsonText);\n// {\"message\":\"He said \\\\\"hello\\\\\"\",\"path\":\"C:\\\\\\\\temp\\\\\\\\a.txt\"}",{"heading":28,"paragraphs":29,"code":32},"二、反斜杠增加是表示层嵌套的必然结果",[30,31],"如果外层对象的 payload 字段要求保存一段“JSON 文本”，就需要先 stringify 内层对象，再 stringify 外层对象。第二次序列化必须保护内层文本里的双引号和反斜杠，因此日志中会看到更多斜杠。","这些斜杠不是业务数据的一部分，而是当前 JSON 文本用来表达字符串内容的语法字符。每正确 parse 一层，就会移除该层对应的转义。","const payloadObject = {\n  name: 'Tiny',\n  path: 'C:\\\\temp\\\\a.txt',\n};\n\nconst envelopeObject = {\n  event: 'save',\n  payload: JSON.stringify(payloadObject),\n};\n\nconst requestBody = JSON.stringify(envelopeObject);\nconsole.log(requestBody);\n// {\"event\":\"save\",\"payload\":\"{\\\\\"name\\\\\":\\\\\"Tiny\\\\\",\\\\\"path\\\\\":\\\\\"C:\\\\\\\\temp\\\\\\\\a.txt\\\\\"}\"}",{"heading":34,"paragraphs":35,"code":38},"三、正确解析：一层字符串对应一次 JSON.parse",[36,37],"先解析 requestBody 得到外层对象，此时 payload 仍是 string；只有接口契约明确规定 payload 是字符串化 JSON 时，才对 payload 再解析一次。","不要写“反复 parse 直到不报错”的循环。普通字符串如 `\"123\"`、`\"true\"` 或用户输入也可能恰好是合法 JSON，递归猜测会悄悄改变类型。","type Envelope = {\n  event: string;\n  payload: string;\n};\n\ntype Payload = {\n  name: string;\n  path: string;\n};\n\nconst requestBody = JSON.stringify({\n  event: 'save',\n  payload: JSON.stringify({ name: 'Tiny', path: 'C:\\\\temp\\\\a.txt' }),\n});\n\nconst outer = JSON.parse(requestBody) as Envelope;\nconst inner = JSON.parse(outer.payload) as Payload;\n\nconsole.log(inner.name); // \"Tiny\"\nconsole.log(inner.path); // \"C:\\\\temp\\\\a.txt\"",{"heading":40,"paragraphs":41,"code":44},"四、编码端应基于对象序列化，禁止手工拼接",[42,43],"手工拼 JSON 时，用户输入中的引号、反斜杠、换行和控制字符都会破坏语法。正确流程是先构造对象，再由 JSON.stringify 负责每一层转义。","更理想的接口设计是让 payload 直接保持对象，而不是字符串化 JSON。只有消息队列字段、数据库文本列、签名协议或第三方接口明确要求字符串时，才保留双层编码。","const payload = { query: 'name = \"Tiny\"', path: 'C:\\\\data' };\n\n// 优先：payload 直接是对象，只需要序列化一次\nconst preferredBody = JSON.stringify({\n  event: 'search',\n  payload,\n});\n\n// 仅在协议要求 payload 为字符串时使用\nconst legacyBody = JSON.stringify({\n  event: 'search',\n  payload: JSON.stringify(payload),\n});",{"heading":46,"paragraphs":47,"code":49},"五、解析不可信字段时使用明确的字段级函数",[48],"当接口历史数据导致同一字段可能是对象或字符串时，可以在边界层做一次兼容，但必须限定字段和最大层数，并保留失败结果用于告警。不要对整个响应递归扫描所有字符串。","type JsonObject = Record\u003Cstring, unknown>;\n\nfunction parseObjectField(value: unknown): JsonObject {\n  if (value && typeof value === 'object' && !Array.isArray(value)) {\n    return value as JsonObject;\n  }\n\n  if (typeof value !== 'string') {\n    throw new TypeError('payload must be an object or a JSON object string');\n  }\n\n  const parsed: unknown = JSON.parse(value);\n  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n    throw new TypeError('payload JSON must contain an object');\n  }\n\n  return parsed as JsonObject;\n}",{"heading":51,"paragraphs":52,"bullets":55},"六、为什么 replace(/\\\\/g, \"\") 会破坏数据",[53,54],"全局删除反斜杠不仅会移除 JSON 语法转义，还会破坏 Windows 路径、正则文本、换行序列以及字符串中本来需要保留的反斜杠。把 `\\\"` 直接替换成 `\"` 也无法判断它属于哪一层。","如果 JSON.parse 报错，应保留原始文本并按 /articles/json-unexpected-token 的流程定位。只有明确知道输入是某种非标准转义协议时，才能编写针对该协议的转换器。",[56,57,58,59],"不要删除所有反斜杠。","不要对整个响应连续 JSON.parse，直到结果不再是字符串。","不要用字符串是否以 { 开头作为唯一类型判断。","不要把控制台展示中的转义形式误认为网络传输内容。",{"heading":61,"paragraphs":62,"code":65},"七、边界条件与验证方法",[63,64],"测试样本至少包含双引号、反斜杠、换行、Tab、空字符串、null、数组以及 Unicode 字符。验证标准不是“日志看起来斜杠变少”，而是最终对象与最初对象深度相等。","对于需要签名或哈希的协议，还必须明确签名的是对象语义、内层 JSON 文本还是外层请求体；不同空格、键顺序和转义形式会生成不同字节。","const original = {\n  quote: '\"',\n  slash: '\\\\',\n  newline: '\\n',\n  nested: { text: '中文 🚀' },\n};\n\nconst outerText = JSON.stringify({ payload: JSON.stringify(original) });\nconst decodedOuter = JSON.parse(outerText) as { payload: string };\nconst restored = JSON.parse(decodedOuter.payload);\n\nconsole.assert(JSON.stringify(restored) === JSON.stringify(original));",{"heading":67,"paragraphs":68},"八、FAQ 与结论",[69,70,71,72],"问：为什么 Postman、Network 和 console 显示的斜杠数量不同？答：它们可能分别展示原始字节、JSON 文本或调试器的字符串预览。","问：payload 能不能永远直接定义为对象？答：自有 API 通常可以；但数据库文本字段、消息协议或第三方接口可能明确要求字符串。","问：`\\u4e2d` 也是嵌套 JSON 问题吗？答：不一定，它是 JSON Unicode 转义，原理见 /articles/unicode-json-escapes。","结论：反斜杠数量由表示层决定。编码时每层只 stringify 一次，解码时根据契约每层只 parse 一次；能传对象就不要额外字符串化。可使用 /tools/json/json-compress-escape 检查转义结果。",{"title":74,"description":75,"intro":76,"sections":77},"Why Backslashes Multiply in Nested JSON: Layered Escaping and Parsing","Separate in-memory objects, JSON text, embedded JSON strings, and JavaScript source literals. Learn the one-stringify/one-parse-per-layer rule and avoid destructive backslash replacements.","A payload such as `{\"payload\":\"{\\\"name\\\":\\\"Tiny\\\"}\"}` usually does not indicate broken escaping. It means JSON text has been stored inside another JSON string. Every string-syntax boundary must escape quotes and backslashes again. This article focuses on representation layers, not general JSON syntax or Unicode escape semantics.",[78,84,89,94,100,105,114,120],{"heading":79,"paragraphs":80,"code":83},"1. Distinguish values, JSON text, and source literals",[81,82],"The same data can be an in-memory object, JSON.stringify output, or a JavaScript string literal that represents that output. The visible slash count depends on which layer a tool is showing.","Check typeof, length, and direct console output rather than relying only on an inspector preview.","const value = { message: 'He said \"hello\"', path: 'C:\\\\temp\\\\a.txt' };\nconst jsonText = JSON.stringify(value);\n\nconsole.log(typeof value);    // object\nconsole.log(typeof jsonText); // string\nconsole.log(jsonText);",{"heading":85,"paragraphs":86,"code":88},"2. More layers require more escaping",[87],"If an outer payload field stores inner JSON text, stringify the inner object and then stringify the outer object. The second serialization protects the quotes and backslashes inside the embedded text. Those extra characters belong to the current representation, not the final business value.","const payloadObject = { name: 'Tiny', path: 'C:\\\\temp\\\\a.txt' };\nconst envelopeObject = {\n  event: 'save',\n  payload: JSON.stringify(payloadObject),\n};\nconst requestBody = JSON.stringify(envelopeObject);\nconsole.log(requestBody);",{"heading":90,"paragraphs":91,"code":93},"3. Parse exactly once per documented string layer",[92],"Parse the outer request first. The payload remains a string until the contract says it contains serialized JSON. Do not repeatedly parse every string until parsing fails; ordinary user text such as “123” or “true” can also be valid JSON and would silently change type.","type Envelope = { event: string; payload: string };\ntype Payload = { name: string; path: string };\n\nconst requestBody = JSON.stringify({\n  event: 'save',\n  payload: JSON.stringify({ name: 'Tiny', path: 'C:\\\\temp\\\\a.txt' }),\n});\nconst outer = JSON.parse(requestBody) as Envelope;\nconst inner = JSON.parse(outer.payload) as Payload;\n\nconsole.log(inner.path); // C:\\\\temp\\\\a.txt",{"heading":95,"paragraphs":96,"code":99},"4. Serialize objects instead of concatenating JSON",[97,98],"Manual concatenation breaks on quotes, backslashes, line breaks, and control characters. Construct values as objects and let JSON.stringify escape each layer.","For APIs you control, prefer a nested object over a stringified payload. Keep double encoding only when a queue field, database text column, signature format, or third-party contract requires it.","const payload = { query: 'name = \"Tiny\"', path: 'C:\\\\data' };\n\nconst preferredBody = JSON.stringify({ event: 'search', payload });\nconst legacyBody = JSON.stringify({\n  event: 'search',\n  payload: JSON.stringify(payload),\n});",{"heading":101,"paragraphs":102,"code":104},"5. Use field-specific compatibility parsing",[103],"If a legacy field may be either an object or a JSON object string, normalize that field once at the boundary and validate the result shape. Do not recursively inspect every string in the response.","type JsonObject = Record\u003Cstring, unknown>;\n\nfunction parseObjectField(value: unknown): JsonObject {\n  if (value && typeof value === 'object' && !Array.isArray(value)) {\n    return value as JsonObject;\n  }\n  if (typeof value !== 'string') {\n    throw new TypeError('payload must be an object or JSON object text');\n  }\n  const parsed: unknown = JSON.parse(value);\n  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n    throw new TypeError('payload JSON must contain an object');\n  }\n  return parsed as JsonObject;\n}",{"heading":106,"paragraphs":107,"bullets":109},"6. Why global backslash replacement corrupts data",[108],"Removing every backslash damages Windows paths, regex text, newline sequences, and legitimate literal backslashes. Replacing escaped quotes without knowing the current layer is equally unsafe. Preserve the raw text and use the workflow in /articles/json-unexpected-token when parsing fails.",[110,111,112,113],"Do not remove all backslashes.","Do not repeatedly JSON.parse every string.","Do not infer a JSON object only because text starts with `{`.","Do not confuse an inspector preview with the network payload.",{"heading":115,"paragraphs":116,"code":119},"7. Boundaries and verification",[117,118],"Test quotes, backslashes, line breaks, tabs, empty strings, null, arrays, and Unicode. Success means the restored value deep-equals the original value, not that a log appears to contain fewer slashes.","For signed or hashed protocols, define which exact representation is signed. Object semantics, inner JSON text, and outer request bytes are different inputs.","const original = {\n  quote: '\"',\n  slash: '\\\\',\n  newline: '\\n',\n  nested: { text: '中文 🚀' },\n};\n\nconst text = JSON.stringify({ payload: JSON.stringify(original) });\nconst outer = JSON.parse(text) as { payload: string };\nconst restored = JSON.parse(outer.payload);\n\nconsole.assert(JSON.stringify(restored) === JSON.stringify(original));",{"heading":121,"paragraphs":122},"8. FAQ and conclusion",[123,124,125,126],"Why do Postman, Network, and console show different slash counts? They may show bytes, JSON text, or an escaped debugger preview.","Can payload always be an object? Usually for your own API, but some storage and third-party contracts require text.","Is `\\u4e2d` the same problem? Not necessarily; it is a JSON Unicode escape covered by /articles/unicode-json-escapes.","The rule is one stringify per encoded layer and one parse per documented string layer. Avoid extra stringification when an object can be transmitted directly, and use /tools/json/json-compress-escape to inspect representation changes.",{"name":128,"url":129},"Tiny's Tool Editorial Team","https://tinystool.org/about",{"name":131,"url":129},"Tiny's Tool Technical Review","2026-08-08",[134],{"title":135,"url":136,"publisher":137},"RFC 8259 — The JavaScript Object Notation (JSON) Data Interchange Format","https://www.rfc-editor.org/rfc/rfc8259","RFC Editor",false,[140,157,172],{"slug":141,"noIndex":142,"category":143,"date":7,"updatedDate":7,"reviewedDate":132,"readingMinutes":144,"tags":145,"relatedTool":15,"cn":151,"en":154},"json-compression-transport",true,"最佳实践",6,[146,147,148,149,150],"HTTP","Content-Encoding","Gzip","Brotli","NDJSON",{"title":152,"description":153},"JSON HTTP 传输：Gzip、Brotli 与流式响应的选择","区分 JSON 压缩、转义和 HTTP Content-Encoding，说明 Gzip、Brotli、分页与 NDJSON 的适用边界，并给出可验证的浏览器流式读取示例。",{"title":155,"description":156},"JSON over HTTP: Choosing Gzip, Brotli, and Streaming","Separate JSON minification and escaping from HTTP Content-Encoding, then choose Gzip, Brotli, pagination, or NDJSON with a verifiable browser streaming example.",{"slug":158,"noIndex":138,"category":6,"date":7,"updatedDate":7,"reviewedDate":132,"readingMinutes":159,"tags":160,"relatedTool":165,"cn":166,"en":169},"json-number-precision",8,[10,161,162,163,164],"JavaScript","Number","BigInt","精度丢失","/tools/json/json-validator",{"title":167,"description":168},"JSON 大整数精度丢失：Number 安全范围与解决方案","说明 JSON 大整数进入 JavaScript 后为什么会被舍入，演示 Number.MAX_SAFE_INTEGER 边界，并给出字符串契约、BigInt 转换和序列化的可验证方案。",{"title":170,"description":171},"Large Integers in JSON: JavaScript Precision Limits and Fixes","Learn why valid JSON integers can be rounded when parsed into JavaScript Number values, and apply string contracts, BigInt conversion, and safe serialization without losing digits.",{"slug":173,"noIndex":138,"category":174,"date":7,"updatedDate":7,"reviewedDate":132,"readingMinutes":175,"tags":176,"relatedTool":165,"cn":181,"en":184},"json-unexpected-token","错误排查",10,[10,177,178,179,180],"JSON.parse","SyntaxError","Unexpected token","调试",{"title":182,"description":183},"JSON Unexpected token：8 类原因与定位","从原始响应、错误位置和 JSON 语法三层定位 JSON.parse 的 Unexpected token，覆盖 HTML 响应、重复解析、尾逗号、引号、控制字符、非法数字和截断数据。",{"title":185,"description":186},"JSON.parse Unexpected Token: 8 Causes and a Reliable Debugging Flow","Debug JSON.parse syntax failures by inspecting the raw response, input type, error position, and JSON grammar. Covers HTML responses, double parsing, trailing commas, escaping, invalid numbers, and truncated payloads.",1786725508293]