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

SpringBoot中实现外部JSON文件到指定格式的映射

问题

我有一个从逗号分隔的CSV转换而来的外部JSON文件,需要在SpringBoot中读取这个结构不直观的JSON,然后将其转换为指定的测试用例格式。

目标格式

{
    "testCases": [
        {
            "Test_Scenario": "GreenMile",
            "Test_Case": "GM_Login",
            "Step_No": "",
            "Status": "Pass",
            "Description": "",
            "Detailed Description": "",
            "Expected_Data": "",
            "Actual_Data": "",
            "Execution_Date": "08_24_2022",
            "Execution_Time": "0:0:10",
            "Error_Screenshot": "",
            "LastWebPage": ""
        },
        {
            "Test_Scenario": "Aries",
            "Test_Case": "AriesCreateUser",
            "Step_No": "",
            "Status": "Pass",
            "Description": "",
            "Detailed Description": "",
            "Expected_Data": "",
            "Actual_Data": "",
            "Execution_Date": "08_24_2022",
            "Execution_Time": "0:0:10",
            "Error_Screenshot": "",
            "LastWebPage": ""
        }
    ]
}

注:原目标格式存在语法错误,已修正为标准JSON格式

待转换的JSON数据

[ {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "GreenMile~GM_Login~~Pass~~~~~08_24_2022~0:0:6~~",
  "" : ""
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "",
  "" : ""
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "~TC01~1~Pass~LAUNCH url=>https://glccd-test.greenmile.com/#/login~LAUNCH url=>https://glccd-test.greenmile.com/#/login~~~08_24_2022~~~null",
  "" : ""
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "~TC01~2~Pass~SET uname=>psingh~SET uname=>psingh~~~08_24_2022~~~null",
  "" : ""
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "~TC01~3~Pass~SET pass=>coke123~SET pass=>coke123~~~08_24_2022~~~null",
  "" : ""
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "~TC01~4~Pass~CLICK //*[@id=\"LoginBox\"]/div[1]/button~CLICK //*[@id=\"LoginBox\"]/div[1]/button~~~08_24_2022~~~null",
  "" : ""
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "",
  "" : ""
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "GreenMile~GM_CreateUser~~Fail~~~~~08_24_2022~0:1:4~~",
  "" : ""
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "",
  "" : ""
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "~TC01~1~Fail~Object Not found: Identifier=XPATH Identifier Value=//*[contains(text()",
  "" : "Administration)] Test Data=~Object Not found: Identifier=XPATH Identifier Value=//*[contains(text()"
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "",
  "" : ""
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "GreenMile~GM_Logout~~Skipped~\"Skipped due to failure in previous case\"~~~~08_24_2022",
  "" : ""
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "",
  "" : ""
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "",
  "" : ""
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "GreenMile~GM_Login~~Skipped~\"Skipped due to failure in previous case\"~~~~08_24_2022",
  "" : ""
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "",
  "" : ""
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "",
  "" : ""
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "GreenMile~GM_ResetPassword~~Skipped~\"Skipped due to failure in previous case\"~~~~08_24_2022",
  "" : ""
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "",
  "" : ""
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "",
  "" : ""
}, {
  "Test_Scenario~Test_Case~Step_No~Status~Description~Detailed Description~Expected_Data~Actual_Data~Execution_Date~Execution_Time~Error_Screenshot~LastWebPage" : "GreenMile~GM_Logout~~Skipped~\"Skipped due to failure in previous case\"~~~~08_24_2022",
  "" : ""
} ]
解决方案

1. 定义目标实体类

创建对应目标格式的Java实体类,字段与目标属性一一对应:

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

@Data
public class TestCase {
    private String Test_Scenario;
    private String Test_Case;
    private String Step_No;
    private String Status;
    private String Description;
    @JsonProperty("Detailed Description")
    private String DetailedDescription;
    private String Expected_Data;
    private String Actual_Data;
    private String Execution_Date;
    private String Execution_Time;
    private String Error_Screenshot;
    private String LastWebPage;
}

注:使用@JsonProperty处理带空格的字段名;若未用Lombok,需自行添加getter/setter方法

2. 读取JSON文件

在SpringBoot中通过ResourceLoader读取外部JSON文件:

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.List;
import java.util.Map;

@Service
public class TestCaseService {
    private final ObjectMapper objectMapper;
    private final ResourceLoader resourceLoader;

    public TestCaseService(ObjectMapper objectMapper, ResourceLoader resourceLoader) {
        this.objectMapper = objectMapper;
        this.resourceLoader = resourceLoader;
    }

    public List<Map<String, String>> readRawJson() throws IOException {
        Resource resource = resourceLoader.getResource("classpath:test-cases.json");
        return objectMapper.readValue(resource.getInputStream(), new TypeReference<List<Map<String, String>>>() {});
    }
}

3. 核心转换逻辑

处理原始JSON的特殊结构,拆分~分隔的键值对,同时处理空行和截断的错误信息:

import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public List<TestCase> convertToTargetFormat() throws IOException {
    List<Map<String, String>> rawData = readRawJson();
    List<TestCase> result = new ArrayList<>();
    // 定义字段顺序,与原始JSON的键分隔结果对应
    String[] fieldNames = {"Test_Scenario", "Test_Case", "Step_No", "Status", "Description", "Detailed Description", "Expected_Data", "Actual_Data", "Execution_Date", "Execution_Time", "Error_Screenshot", "LastWebPage"};

    for (Map<String, String> entry : rawData) {
        // 获取主字段的键(带~的那个)
        String mainKey = entry.keySet().stream()
                .filter(k -> k.contains("~"))
                .findFirst()
                .orElse("");
        String mainValue = entry.get(mainKey);
        String emptyValue = entry.get("");

        // 跳过完全空的条目
        if (StringUtils.isBlank(mainValue) && StringUtils.isBlank(emptyValue)) {
            continue;
        }

        TestCase testCase = new TestCase();
        // 拆分主值为字段数组
        String[] values = StringUtils.splitByWholeSeparator(mainValue, "~");

        // 映射字段值
        for (int i = 0; i < fieldNames.length; i++) {
            String value = i < values.length ? values[i] : "";
            // 处理特殊字段:Detailed Description
            if ("Detailed Description".equals(fieldNames[i])) {
                // 合并截断的错误信息
                if ("Fail".equals(testCase.getStatus()) && StringUtils.isNotBlank(emptyValue)) {
                    value += emptyValue;
                }
                testCase.setDetailedDescription(value);
            } else {
                // 手动映射字段值
                switch (fieldNames[i]) {
                    case "Test_Scenario":
                        testCase.setTest_Scenario(value);
                        break;
                    case "Test_Case":
                        testCase.setTest_Case(value);
                        break;
                    case "Step_No":
                        testCase.setStep_No(value);
                        break;
                    case "Status":
                        testCase.setStatus(value);
                        break;
                    case "Description":
                        testCase.setDescription(value);
                        break;
                    case "Expected_Data":
                        testCase.setExpected_Data(value);
                        break;
                    case "Actual_Data":
                        testCase.setActual_Data(value);
                        break;
                    case "Execution_Date":
                        testCase.setExecution_Date(value);
                        break;
                    case "Execution_Time":
                        testCase.setExecution_Time(value);
                        break;
                    case "Error_Screenshot":
                        testCase.setError_Screenshot("null".equals(value) ? "" : value);
                        break;
                    case "LastWebPage":
                        testCase.setLastWebPage("null".equals(value) ? "" : value);
                        break;
                }
            }
        }

        // 为步骤类用例补充缺失的Test_Scenario
        if (StringUtils.isBlank(testCase.getTest_Scenario()) && !result.isEmpty()) {
            TestCase lastValidScenario = result.stream()
                    .filter(t -> StringUtils.isNotBlank(t.getTest_Scenario()))
                    .reduce((first, second) -> second)
                    .orElse(null);
            if (lastValidScenario != null) {
                testCase.setTest_Scenario(lastValidScenario.getTest_Scenario());
            }
        }

        result.add(testCase);
    }

    return result;
}

4. 输出转换结果

将转换后的列表转为标准JSON输出:

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public String getFormattedTestCases() throws IOException {
    List<TestCase> testCases = convertToTargetFormat();
    Map<String, List<TestCase>> wrapper = new HashMap<>();
    wrapper.put("testCases", testCases);
    return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(wrapper);
}

关键处理点

  • 跳过完全空的条目,避免无效数据
  • 合并截断的错误信息(空键对应的内容)
  • 为步骤类用例补充缺失的Test_Scenario(继承上一个有效场景)
  • 将null字符串转为空字符串,符合目标格式要求

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.21 01:36:34