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
相关产品推荐
相关产品推荐

