Spring Boot中构建并序列化嵌套结构JSON的最佳实践
针对你遇到的这种带点分隔的复合键转嵌套JSON的需求,我来给你梳理几个实用的解决方案,既能解决类型转换的痛点,又能完美适配Spring Boot的序列化流程:
方案一:递归构建类型安全的嵌套Map结构
虽然直接用Map<String, Object>会有未检查类型转换的问题,但我们可以封装一个工具类,通过递归逻辑自动处理键的拆分和嵌套结构的构建,把类型转换的逻辑封装在内部,对外提供安全的API。
实现代码
import java.util.*; public class NestedDataBuilder { public static Map<String, Object> buildFromFlatPairs(Map<String, String> flatKeyValuePairs) { Map<String, Object> rootMap = new HashMap<>(); for (Map.Entry<String, String> entry : flatKeyValuePairs.entrySet()) { String[] keySegments = entry.getKey().split("\\."); String value = entry.getValue(); // 针对你示例中的三层键结构(协议.站点.域名后缀)处理 String protocol = keySegments[0]; String site = keySegments[1]; String tld = keySegments[2]; // 获取或创建协议对应的数组 List<Map<String, Object>> protocolArray = (List<Map<String, Object>>) rootMap.computeIfAbsent( protocol, k -> new ArrayList<>() ); // 构建站点-域名后缀的嵌套对象 Map<String, Object> siteEntry = new HashMap<>(); Map<String, String> tldEntry = new HashMap<>(); tldEntry.put(tld, value); siteEntry.put(site, tldEntry); protocolArray.add(siteEntry); } return rootMap; } }
使用方式
在Spring Boot的Controller里直接返回这个Map即可,Jackson会自动将其序列化为你想要的JSON格式:
@RestController public class DataController { @GetMapping("/nested-data") public Map<String, Object> getNestedData() { Map<String, String> flatData = new HashMap<>(); flatData.put("https.stackoverflow.com", "1"); flatData.put("https.facebook.com", "2"); flatData.put("https.something.de", "3"); return NestedDataBuilder.buildFromFlatPairs(flatData); } }
这个方案的优势是灵活,不管键的层级后续是否扩展,都可以调整递归逻辑适配,而且完全兼容Spring Boot的默认序列化机制。
方案二:使用Jackson原生节点API(类型安全首选)
既然Spring Boot默认用Jackson做序列化,直接用Jackson提供的ObjectNode和ArrayNode来构建JSON结构是最稳妥的选择——完全没有类型转换问题,而且原生支持序列化。
实现代码
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import java.util.Map; public class JacksonNodeDataBuilder { public static ObjectNode buildNestedJson(Map<String, String> flatData) { ObjectNode rootNode = Jackson2ObjectMapperBuilder.json().build().createObjectNode(); for (Map.Entry<String, String> entry : flatData.entrySet()) { String[] keySegments = entry.getKey().split("\\."); String value = entry.getValue(); // 获取或创建"https"对应的数组节点 ArrayNode httpsArray; if (rootNode.has("https")) { httpsArray = (ArrayNode) rootNode.get("https"); } else { httpsArray = rootNode.putArray("https"); } // 构建站点节点和域名后缀节点 ObjectNode siteNode = Jackson2ObjectMapperBuilder.json().build().createObjectNode(); ObjectNode tldNode = Jackson2ObjectMapperBuilder.json().build().createObjectNode(); tldNode.put(keySegments[2], value); siteNode.set(keySegments[1], tldNode); httpsArray.add(siteNode); } return rootNode; } }
使用方式
Controller里直接返回ObjectNode即可,Spring会自动处理成JSON响应:
@RestController public class DataController { @GetMapping("/nested-data") public ObjectNode getNestedData() { Map<String, String> flatData = new HashMap<>(); flatData.put("https.stackoverflow.com", "1"); flatData.put("https.facebook.com", "2"); flatData.put("https.something.de", "3"); return JacksonNodeDataBuilder.buildNestedJson(flatData); } }
这个方案的优势是完全类型安全,所有节点的操作都是明确的Jackson API,不会出现未检查转换的警告,而且序列化效率最高。
方案三:自定义DTO类(适合固定结构场景)
如果你的复合键结构是固定的(比如永远是三层:协议.站点.域名后缀),那么自定义DTO类是最清晰的方案,完全没有类型问题,可读性也最强。
实现代码
import java.util.List; public class ProtocolDataDto { private List<SiteDto> https; // Getter & Setter public List<SiteDto> getHttps() { return https; } public void setHttps(List<SiteDto> https) { this.https = https; } public static class SiteDto { private TldDto stackoverflow; private TldDto facebook; private TldDto something; // Getters & Setters public TldDto getStackoverflow() { return stackoverflow; } public void setStackoverflow(TldDto stackoverflow) { this.stackoverflow = stackoverflow; } public TldDto getFacebook() { return facebook; } public void setFacebook(TldDto facebook) { this.facebook = facebook; } public TldDto getSomething() { return something; } public void setSomething(TldDto something) { this.something = something; } public static class TldDto { private String com; private String de; // Getters & Setters public String getCom() { return com; } public void setCom(String com) { this.com = com; } public String getDe() { return de; } public void setDe(String de) { this.de = de; } } } }
使用方式
在Controller里构建DTO实例并返回:
@RestController public class DataController { @GetMapping("/nested-data") public ProtocolDataDto getNestedData() { ProtocolDataDto dto = new ProtocolDataDto(); List<ProtocolDataDto.SiteDto> siteList = new ArrayList<>(); // 构建stackoverflow条目 ProtocolDataDto.SiteDto stackoverflowSite = new ProtocolDataDto.SiteDto(); ProtocolDataDto.TldDto stackoverflowTld = new ProtocolDataDto.TldDto(); stackoverflowTld.setCom("1"); stackoverflowSite.setStackoverflow(stackoverflowTld); siteList.add(stackoverflowSite); // 构建facebook条目 ProtocolDataDto.SiteDto facebookSite = new ProtocolDataDto.SiteDto(); ProtocolDataDto.TldDto facebookTld = new ProtocolDataDto.TldDto(); facebookTld.setCom("2"); facebookSite.setFacebook(facebookTld); siteList.add(facebookSite); // 构建something条目 ProtocolDataDto.SiteDto somethingSite = new ProtocolDataDto.SiteDto(); ProtocolDataDto.TldDto somethingTld = new ProtocolDataDto.TldDto(); somethingTld.setDe("3"); somethingSite.setSomething(somethingTld); siteList.add(somethingSite); dto.setHttps(siteList); return dto; } }
这个方案的缺点是灵活性差,如果后续键的结构有变化(比如新增其他协议、其他站点),需要修改DTO类,但胜在类型安全、可读性强,适合固定业务场景。
总结
- 如果键的结构不固定、需要灵活扩展:优先选择方案二(Jackson节点API),类型安全且适配Spring Boot序列化;或者方案一,更灵活但需要封装好类型转换逻辑。
- 如果键的结构固定:选择方案三(自定义DTO),代码可读性和维护性最佳。
- 避免使用Gson的
JsonObject,因为Spring Boot默认序列化器是Jackson,两者兼容性不好,容易出现序列化失败的问题。
内容的提问来源于stack exchange,提问作者WildWind03

