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

Spring Boot中构建并序列化嵌套结构JSON的最佳实践

处理复合键转嵌套JSON的Spring Boot最佳实践

针对你遇到的这种带点分隔的复合键转嵌套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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 08:41:32