You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在加载JSON字符串时保持原有键顺序?

问题描述

我从API加载JSON字符串,用以下代码格式化后加载到可编辑DIV中:

JSON.stringify(JSON.parse(rawJson.replace(/\s+/g, ' ').trim()), null, 1)

目前遇到的问题是:JSON字符串显示时无法保留原始的键顺序,加载后的顺序是混乱的。

处理JSON的函数如下:

// process incoming JSON template
const processJSON = async (json: any, keys?: any[]) => {
  if (json) {
    let _matches = []
    let _unMatch = [];

    // format json for display
    const jsonString = json && JSON.stringify(json, null, 1);
    // update the match keys with default values

    if (!keys) {
      // get all default keys
      const matches = jsonString.match(/{{(([^}][^}]?|[^}]}?)*)}}/gm);

      const defaultMatchClean = matches && matches.map(match => {
        //return match
        return match.replace(/{{|}}/gm, '')
      })

      defaultMatchClean && defaultMatchClean.forEach(match => {
        _matches = [..._matches, { selectedString: match, key: match }]
        _unMatch = [..._unMatch, { key: match, html: match }]
      })

      // rebuild display with default values
      if (matches) {
        matchKeys.current = _matches;
        unMatch.current = _unMatch
      }

      selectedRef.current = jsonString;
      setSelectedJSON(jsonString);
      console.log("🚀 ~ file: AlertBody.tsx ~ line 121 ~ processJSON ~ json", JSON.stringify(json), _matches)
      return
    }

    matchKeys.current = keys ? keys : _matches;;
    matchKeys.current.forEach(match => {
      _unMatch = match.key.includes('pw') ? [..._unMatch, { key: match.key, html: match.key }] :
        [..._unMatch, { key: match.selectedString, html: match.key }]
    })

    unMatch.current = _unMatch
    selectedRef.current = jsonString;

    setSelectedJSON(jsonString);
    console.log("🚀 ~ file: AlertBody.tsx ~ line 121 ~ processJSON ~ json", jsonString)
  }
}

示例JSON:

{ "blocks": [{"text": {"type": "plain_text","text": "{{pw.alert.severity}} - {{pw.alert.title}}"},"type": "header"} ], "attachments": [{"color": "{{pw.state.severityColor}} - {{pw.alert.title}}","blocks": [{"type": "section","text": {"type": "plain_text","text": "Triggered: {{pw.alert.timestamp.iso}}"}},{"type": "section","text": {"text": "{{pw.alert.description}}","type": "mrkdwn"}},{"type": "actions","elements": [{"type": "button","value": "View Alert","action_id": "button-action-1","text": {"text": "View Alert","type": "plain_text","emoji": true},"url": "{{pw.alert.alertUrl}}"}]},{"type": "header","text": {"type": "plain_text","text": "CORRELATIONS"}},{"type": "section","text": {"type": "mrkdwn","text": "<{{pw.link.openall}} | *Open All*> | <{{pw.link.downloadall}} | *Download All*>"}},{"text": {"type": "plain_text","emoji": true,"text": "{{pw.workflow.destinationGroup.1.name}}"},"type": "header"},{"type": "divider"},{"type": "section","text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.1.destination.0.url}} | * {{pw.workflow.destinationGroup.1.destination.0.name}}*>"}},{"text": {"text": "<{{pw.workflow.destinationGroup.1.destination.1.url}} | * {{pw.workflow.destinationGroup.1.destination.1.name}}*>","type": "mrkdwn"},"type": "section"},{"type": "section","text": {"text": "<{{pw.workflow.destinationGroup.1.destination.2.url}} | * {{pw.workflow.destinationGroup.1.destination.2.name}}*>","type": "mrkdwn"}},{"text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.1.destination.3.url}} | * {{pw.workflow.destinationGroup.1.destination.3.name}}*>"},"type": "section"},{"type": "section","text": {"text": "<{{pw.workflow.destinationGroup.1.destination.4.url}} | * {{pw.workflow.destinationGroup.1.destination.4.name}}*>","type": "mrkdwn"}},{"type": "section","text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.1.destination.5.url}} | * {{pw.workflow.destinationGroup.1.destination.5.name}}*>"},"type": "section"},{"text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.1.destination.6.url}} | * {{pw.workflow.destinationGroup.1.destination.6.name}}*>"},"type": "section"},{"type": "section","text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.1.destination.7.url}} | * {{pw.workflow.destinationGroup.1.destination.7.name}}*>"},"type": "section"},{"type": "section","text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.1.destination.8.url}} | * {{pw.workflow.destinationGroup.1.destination.8.name}}*>"},"type": "section"},{"type": "section","text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.1.destination.9.url}} | * {{pw.workflow.destinationGroup.1.destination.9.name}}*>"},"type": "section"},{"type": "header","text": {"emoji": true,"text": "{{pw.workflow.destinationGroup.2.name}}","type": "plain_text"}},{"type": "divider"},{"text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.2.destination.0.url}} | * {{pw.workflow.destinationGroup.2.destination.0.name}}*>"},"type": "section"},{"text": {"type": "plain_text","text": "{{pw.workflow.destinationGroup.3.name}}","emoji": true},"type": "header"},{"type": "divider"},{"text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.3.destination.0.url}} | * {{pw.workflow.destinationGroup.3.destination.0.name}}*>"},"type": "section"},{"type": "section","text": {"text": "<{{pw.workflow.destinationGroup.3.destination.1.url}} | * {{pw.workflow.destinationGroup.3.destination.1.name}}*>","type": "mrkdwn"}},{"text": {"text": "<{{pw.workflow.destinationGroup.3.destination.2.url}} | * {{pw.workflow.destinationGroup.3.destination.2.name}}*>","type": "mrkdwn"},"type": "section"},{"type": "header","text": {"emoji": true,"type": "plain_text","text": "{{pw.workflow.destinationGroup.4.name}}"}},{"type": "divider"},{"type": "section","text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.4.destination.0.url}} | * {{pw.workflow.destinationGroup.4.destination.0.name}}*>"}},{"text": {"text": "<{{pw.workflow.destinationGroup.4.destination.1.url}} | * {{pw.workflow.destinationGroup.4.destination.1.name}}*>","type": "mrkdwn"},"type": "section"},{"text": {"text": "<{{pw.workflow.destinationGroup.4.destination.2.url}} | * {{pw.workflow.destinationGroup.4.destination.2.name}}*>","type": "mrkdwn"},"type": "section"},{"type": "section","text": {"text": "<{{pw.workflow.destinationGroup.4.destination.3.url}} | * {{pw.workflow.destinationGroup.4.destination.3.name}}*>","type": "mrkdwn"}},{"type": "header","text": {"type": "plain_text","text": ":camera: Snapshots","emoji": true}},{"type": "divider"},{"type": "section","text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.1.destination.0.snapshot.protectedUrl}} | *High-Resolution Image: {{pw.workflow.destinationGroup.1.destination.0.name}}*>"}},{"type": "image","title": {"type": "plain_text","emoji": true,"text": "{{pw.workflow.destinationGroup.1.destination.0.name}}"},"alt_text": "{{pw.workflow.destinationGroup.1.destination.0.name}}","image_url": "{{pw.workflow.destinationGroup.1.destination.0.snapshot.publicUrl}}"},{"type": "section","text": {"text": "<{{pw.workflow.destinationGroup.1.destination.3.snapshot.protectedUrl}} | *High-Resolution Image: {{pw.workflow.destinationGroup.1.destination.3.name}}*>","type": "mrkdwn"}},{"title": {"emoji": true,"text": "{{pw.workflow.destinationGroup.1.destination.3.name}}","type": "plain_text"},"alt_text": "{{pw.workflow.destinationGroup.1.destination.3.name}}","type": "image","image_url": "{{pw.workflow.destinationGroup.1.destination.3.snapshot.publicUrl}}"}]} ]}
解决方案

问题核心是:普通JS对象不保证键的顺序(ES6+虽有排序规则,但和原始JSON的顺序未必一致),当你用JSON.parse把字符串转成普通对象后,再用JSON.stringify输出,键的顺序就会被打乱。

以下是两种可行的解决方法:

方案1:直接格式化原始JSON字符串,避免解析为普通对象

既然要保留原始顺序,没必要先解析成对象再转回去。可以直接对原始rawJson进行格式化,比如使用prettier的JSON格式化能力,它会严格保留原始键顺序:

// 安装依赖:npm install prettier prettier/parser-json
import prettier from "prettier";
import parserJson from "prettier/parser-json";

// 替换原来的JSON.stringify(JSON.parse(...), null, 1)
const formattedJson = prettier.format(rawJson.replace(/\s+/g, ' ').trim(), {
  parser: "json",
  plugins: [parserJson],
  tabWidth: 1,
});

方案2:用Map保留顺序,自定义stringify函数

如果必须解析成对象处理,可以用JSON.parse的reviver参数将对象转为Map(Map会严格保留插入顺序),再自定义一个能处理Map的stringify函数:

// 解析时生成有序Map
const parseWithOrder = (jsonStr) => {
  return JSON.parse(jsonStr, (key, value) => {
    if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
      return new Map(Object.entries(value));
    }
    return value;
  });
};

// 自定义stringify处理Map
const stringifyWithOrder = (value, indent = 1) => {
  if (value instanceof Map) {
    const entries = Array.from(value.entries());
    const indentStr = ' '.repeat(indent);
    const nextIndent = indent + 1;
    return `{\n${entries.map(([k, v]) => 
      `${indentStr}"${k}": ${stringifyWithOrder(v, nextIndent)}`
    ).join(',\n')}\n${' '.repeat(indent-1)}}`;
  } else if (Array.isArray(value)) {
    const indentStr = ' '.repeat(indent);
    const nextIndent = indent + 1;
    return `[\n${value.map(item => 
      `${indentStr}${stringifyWithOrder(item, nextIndent)}`
    ).join(',\n')}\n${' '.repeat(indent-1)}]`;
  } else if (typeof value === 'string') {
    return `"${value.replace(/"/g, '\\"')}"`;
  } else {
    return JSON.stringify(value);
  }
};

// 使用示例
const parsed = parseWithOrder(rawJson.replace(/\s+/g, ' ').trim());
const formattedJson = stringifyWithOrder(parsed, 1);

针对你的代码修改建议

如果采用方案1,需要调整processJSON的参数,传入原始JSON字符串而非解析后的对象:

const processJSON = async (rawJson: string, keys?: any[]) => {
  if (rawJson) {
    let _matches = [];
    let _unMatch = [];

    // 直接格式化原始JSON,保留顺序
    import prettier from "prettier";
    import parserJson from "prettier/parser-json";
    const jsonString = prettier.format(rawJson.replace(/\s+/g, ' ').trim(), {
      parser: "json",
      plugins: [parserJson],
      tabWidth: 1,
    });

    if (!keys) {
      // 后续逻辑保持不变
      const matches = jsonString.match(/{{(([^}][^}]?|[^}]}?)*)}}/gm);
      // ... 其余代码原样保留
    }

    // ... 其余代码原样保留
  }
};

这样处理后,JSON的键顺序就会和API返回的原始数据完全一致。


内容的提问来源于stack exchange,提问作者Rayon Hunte

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 09:10:35