Gson处理动态JSON结构:单对象与数组的统一解析策略

Gson处理动态JSON结构:单对象与数组的统一解析策略

针对JSON数据中某个字段可能为单个对象或对象数组的动态结构,本文提供了一种基于Gson库的通用解析策略。通过实现自定义的TypeAdapterFactory,在反序列化时智能判断JSON令牌类型,并将单对象自动包装成数组,从而确保数据模型能够统一处理这两种情况,避免解析错误。

在实际开发中,我们经常会遇到第三方api返回的json数据结构不一致的情况。例如,某个表示“报告列表”的字段,在只有一条报告时可能直接返回一个json对象,而在有多条报告时则返回一个json数组。这给传统的java对象映射(pojo)带来了挑战,因为pojo的字段类型通常是固定的(如list或radarreport)。直接使用gson进行反序列化时,如果json结构与pojo定义不符,就会导致解析失败。

以下是两种常见的JSON结构示例,其中RadarReport字段的类型是动态变化的:

示例1:RadarReport为单个对象

{"RadarReports": {    "executionTime": "135",    "RadarReport": {        "abc": "1116591",        "name": "abc",        "id": "2019050311582056119",        "ownerId": "xyz"    },    "size" :"1"}}

示例2:RadarReport为对象数组

{"RadarReports": {    "executionTime": "113",    "RadarReport": [        {            "abc": "1116591",            "name": "abc",            "id": "2019050311582056119",            "ownerId": "xyz"        },        {            "abc": "1116591",            "name": "abc",            "id": "2019050311582056119",            "ownerId": "xyz"        }    ],    "size" : "2"}}

为了统一处理这两种情况,我们可以利用Gson提供的扩展机制——自定义TypeAdapterFactory。

解决方案:自定义TypeAdapterFactory

TypeAdapterFactory是一个接口,允许我们为特定类型创建自定义的TypeAdapter。TypeAdapter则负责定义Java对象与JSON数据之间的序列化和反序列化逻辑。

核心思想是:在反序列化RadarReport字段时,我们首先检查当前JSON令牌的类型。如果它是一个JSON对象(BEGIN_OBJECT),我们就将其读取为一个JsonObject,然后手动将其包装到一个JsonArray中。这样,无论是单个对象还是对象数组,最终都会被转换为JsonArray,然后我们就可以委托给Gson内置的列表适配器进行后续解析。

以下是自定义SingleObjectOrListAdapterFactory的实现:

Jenni AI Jenni AI

使用最先进的 AI 写作助手为您的写作增光添彩。

Jenni AI 48 查看详情 Jenni AI

import com.google.gson.Gson;import com.google.gson.JsonArray;import com.google.gson.JsonObject;import com.google.gson.TypeAdapter;import com.google.gson.TypeAdapterFactory;import com.google.gson.annotations.JsonAdapter;import com.google.gson.reflect.TypeToken;import com.google.gson.stream.JsonReader;import com.google.gson.stream.JsonToken;import com.google.gson.stream.JsonWriter;import java.io.IOException;import java.util.ArrayList;// 仅用于 @JsonAdapter 注解class SingleObjectOrListAdapterFactory implements TypeAdapterFactory {    @Override    public  TypeAdapter create(Gson gson, TypeToken type) {        // 获取处理列表类型(如ArrayList)的默认适配器        // 注意:此处不能直接使用 gson.getDelegateAdapter(this, type)        // 因为可能导致无限递归或与Gson的内部机制冲突(参考GitHub issue 1028)        TypeAdapter listAdapterDelegate = gson.getAdapter(type);        // 获取处理JsonObject的适配器,用于读取单个JSON对象        TypeAdapter jsonObjectAdapter = gson.getAdapter(JsonObject.class);        return new TypeAdapter() {            @Override            public void write(JsonWriter out, T value) throws IOException {                // 序列化逻辑,通常直接委托给默认的列表适配器                listAdapterDelegate.write(out, value);            }            @Override            public T read(JsonReader in) throws IOException {                // 在读取之前,先“窥视”下一个JSON令牌的类型                if (in.peek() == JsonToken.BEGIN_OBJECT) {                    // 如果当前令牌是JSON对象(即单个对象而非数组)                    // 1. 读取该JSON对象                    JsonObject jsonObject = jsonObjectAdapter.read(in);                    // 2. 创建一个新的JSON数组                    JsonArray jsonArray = new JsonArray();                    // 3. 将读取到的单个JSON对象添加到数组中                    jsonArray.add(jsonObject);                    // 4. 将这个包装后的JSON数组委托给原始的列表适配器进行解析                    return listAdapterDelegate.fromJsonTree(jsonArray);                } else {                    // 如果当前令牌不是JSON对象(预期是BEGIN_ARRAY,即数组)                    // 则直接委托给原始的列表适配器进行解析                    return listAdapterDelegate.read(in);                }            }        };    }}

应用自定义适配器

创建了SingleObjectOrListAdapterFactory之后,我们需要将其应用到POJO中对应的字段上。这可以通过@JsonAdapter注解实现。

假设我们的POJO结构如下:

import lombok.Data;import com.google.gson.annotations.JsonAdapter;import java.util.ArrayList;@Datapublic class RadarReport {    private String abc;    private String name;    private String id;    private String ownerId;}@Datapublic class Radarreports {    private int size;    // 使用 @JsonAdapter 注解指定自定义的 TypeAdapterFactory    @JsonAdapter(SingleObjectOrListAdapterFactory.class)    private ArrayList RadarReport; // 注意这里是ArrayList    // 如果存在 RadarReportSet 字段,也需要根据实际情况处理    // private ArrayList RadarReportSet;     private String executionTime; // 添加缺失的字段}@Datapublic class ReportsResponse {    private Radarreports RadarReports;}

请注意,RadarReport字段的类型被定义为ArrayList,并且通过@JsonAdapter注解引用了我们自定义的SingleObjectOrListAdapterFactory。这样,无论JSON中RadarReport是单个对象还是数组,Gson都会通过我们自定义的逻辑进行处理,最终将其解析为ArrayList。

完整示例与注意事项

以下是如何使用Gson进行反序列化的完整示例:

import com.google.gson.Gson;import com.google.gson.GsonBuilder;import java.io.BufferedReader;import java.io.InputStreamReader;import java.net.URL;public class JsonParsingExample {    public static void main(String[] args) {        // 模拟两种JSON输入        String jsonSingleObject = "{"RadarReports": {"executionTime": "135","RadarReport": {"abc": "1116591","name": "abc","id": "2019050311582056119","ownerId": "xyz"},"size" :"1"}}";        String jsonArrayObjects = "{"RadarReports": {"executionTime": "113","RadarReport": [{"abc": "1116591","name": "abc","id": "2019050311582056119","ownerId": "xyz"},{"abc": "1116591","name": "abc","id": "2019050311582056119","ownerId": "xyz"}],"size" : "2"}}";        Gson gson = new GsonBuilder().create();        try {            // 解析单个对象的情况            System.out.println("--- Parsing Single Object JSON ---");            ReportsResponse responseSingle = gson.fromJson(jsonSingleObject, ReportsResponse.class);            System.out.println("Execution Time: " + responseSingle.getRadarReports().getExecutionTime());            System.out.println("Size: " + responseSingle.getRadarReports().getSize());            System.out.println("RadarReport Count: " + responseSingle.getRadarReports().getRadarReport().size());            if (!responseSingle.getRadarReports().getRadarReport().isEmpty()) {                System.out.println("First RadarReport ID: " + responseSingle.getRadarReports().getRadarReport().get(0).getId());            }            System.out.println("n--- Parsing Array of Objects JSON ---");            // 解析数组对象的情况            ReportsResponse responseArray = gson.fromJson(jsonArrayObjects, ReportsResponse.class);            System.out.println("Execution Time: " + responseArray.getRadarReports().getExecutionTime());            System.out.println("Size: " + responseArray.getRadarReports().getSize());            System.out.println("RadarReport Count: " + responseArray.getRadarReports().getRadarReport().size());            if (!responseArray.getRadarReports().getRadarReport().isEmpty()) {                System.out.println("First RadarReport ID: " + responseArray.getRadarReports().getRadarReport().get(0).getId());            }            // 如果是从URL读取,可以这样:            // URL url = new URL(queryUrl);            // BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()));            // ReportsResponse radarReports = gson.fromJson(br, ReportsResponse.class);            // br.close();        } catch (Exception e) {            e.printStackTrace();        }    }}

注意事项:

适用场景: 这种方法特别适用于JSON中某个字段类型不确定,但在Java中希望统一表示为列表的情况。性能考虑: 对于极大规模的JSON数据,频繁地进行peek()、创建JsonObject和JsonArray可能会带来轻微的性能开销。但对于大多数应用场景,这种开销是可接受的。错误处理: 自定义适配器增强了健壮性,但如果JSON结构完全不符合预期(例如,RadarReport字段既不是对象也不是数组,而是字符串或数字),仍可能抛出IOException或其他Gson解析异常,需要进行适当的异常捕获和处理。Gson版本: 确保使用的Gson版本支持TypeAdapterFactory和@JsonAdapter注解。本文示例基于较新的Gson版本。字段名一致性: 确保POJO中的字段名与JSON中的键名保持一致,或者使用@SerializedName注解进行映射。

通过自定义TypeAdapterFactory,我们能够灵活地处理Gson默认解析器无法直接应对的复杂或动态变化的JSON结构,极大地增强了JSON反序列化的健壮性和适应性。这种模式在处理第三方API数据时尤为有用,能够有效避免因数据格式不一致而导致的解析失败。

以上就是Gson处理动态JSON结构:单对象与数组的统一解析策略的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/752866.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年11月25日 21:18:09
下一篇 2025年11月25日 21:18:33

相关推荐

  • XPath的.语法代表当前节点吗?

    .在XPath中代表当前上下文节点,用于基于当前位置进行相对路径导航,可明确指向当前元素以实现精准定位,常用于相对路径、谓语条件判断、函数参数中,如./span表示当前节点下的span子元素,//div[./@id=’main’]表示id属性为main的div,string(…

    2025年12月17日
    000
  • XPath的static-base-uri()函数获取什么?

    static-base-uri()函数为空的情况主要有:XPath表达式在代码中以字符串形式直接定义时,因无关联资源地址而返回空;动态生成的XPath表达式若生成上下文未提供基URI信息,则结果为空;某些XPath引擎实现不完整或未支持该函数时也可能返回空;尽管未声明命名空间不直接导致其为空,但可能…

    2025年12月17日
    000
  • XPath的parse-xml()函数如何解析字符串?

    parse-xml()函数的作用是将XML格式的字符串解析为XPath可操作的文档节点,使其能被路径表达式查询。例如,调用parse-xml($myXmlString)//item[name=’产品甲’]/price/@currency可从解析后的节点树中提取指定数据。该函数…

    2025年12月17日
    000
  • XPath的..语法如何选择父节点?

    ..的核心作用是选中当前节点的直接父节点,如//span/..可选中span的父节点li,连续使用可向上多级跳跃,常用于灵活定位。 XPath中那个看似简单的 .. 语法,其核心作用就是让你从当前所在的节点,向上一步,准确无误地选中它的直接父节点。这在处理XML或HTML文档时,简直是家常便饭,而且…

    2025年12月17日
    000
  • XPath的string-length()函数计算什么?

    string-length()函数用于计算字符串字符数,包括空格和特殊字符,支持Unicode,常用于数据验证、字符串截取、条件判断等场景。 XPath的 string-length() 函数,顾名思义,是用来计算字符串长度的。它会返回一个字符串中字符的数量,这个数量包括空格和其他特殊字符。简单来说…

    2025年12月17日
    000
  • XPath的descendant-or-self轴包含自身吗?

    descendant-or-self轴选取当前节点及其所有后代节点,如下使用descendant-or-self::*可选中自身及、、,而descendant轴不包含自身,self轴仅选自身,结合谓语可实现精准查询。 XPath的 descendant-or-self 轴,顾名思义,包含当前节点自身…

    2025年12月17日
    000
  • XPath的available-environment-variables()?

    available-environment-variables()是Saxon扩展函数,非XPath标准,用于获取环境变量名序列,需结合system-property()获取值,使用时需注意安全风险并限制访问权限。 JAVA_HOME environment variable is not set.…

    2025年12月17日
    000
  • XPath的outermost()函数处理什么节点?

    outermost()函数用于筛选节点序列中最外层的节点,即移除被其他选中节点包含的后代节点,保留不被包含的祖先节点。例如在表达式outermost(//section | //p)中,若包含,则只保留和未被包含的,结果为和。与innermost()相反,后者保留最内层节点。outermost()适…

    2025年12月17日
    000
  • XML的DTD实体注入攻击怎么防范?解析时要注意什么?

    防范XML的DTD实体注入攻击最核心的策略是禁用外部实体解析。具体做法包括在XML解析器中关闭外部实体加载功能,如Java中通过设置SAXParserFactory和DocumentBuilderFactory的特性禁用外部实体、PHP中使用LIBXML_NOENT和LIBXML_NONET选项、P…

    2025年12月17日
    000
  • 如何在Prolog中使用SGML/XML解析库处理语义Web数据?

    答案:选择合适的Prolog库如library(sgml)或library(libxml2),加载并解析XML文档,通过遍历结构或XPath提取RDF三元组,处理命名空间与错误,将三元组用assertz存入知识库或使用索引优化,对大型文件采用流式处理以防内存溢出。 在Prolog中使用SGML/XM…

    2025年12月17日
    000
  • XPath的ancestor-or-self轴包含当前节点吗?

    是的,XPath的ancestor-or-self轴包含当前节点,它与ancestor轴的核心区别在于前者包含自身而后者仅包含祖先节点。当从一个节点出发时,ancestor-or-self会返回该节点及其所有祖先,适用于需要同时检查当前节点和上级节点的场景,如查找具有特定属性的最近容器、判断权限继承…

    2025年12月17日
    000
  • XPath的substring-before()函数怎么用?

    substring-before()用于提取分隔符前的字符串,适用于从XML/HTML中提取前缀信息,如路径、ID等;若分隔符不存在则返回空,且仅匹配首个分隔符,需结合substring-after()处理复杂结构,常用于网页数据清洗。 XPath的 substring-before() 函数,顾名…

    2025年12月17日
    000
  • XPath的function-available()函数如何检查?

    function-available()用于检查XPath函数是否可用,返回布尔值。通过传入函数名字符串如function-available(‘substring’),可判断该函数是否存在,避免运行时错误。常用于编写兼容不同XPath处理器的可移植表达式,例如结合if()函…

    2025年12月17日
    000
  • XPath的tokenize()函数如何分割字符串?

    tokenize()函数用于将字符串按分隔符分割成序列,支持正则表达式分隔符,可处理连续或首尾分隔符产生的空字符串,常用于解析XML中多值属性或元素内容,如作者、标签、颜色等,需配合string-length或normalize-space过滤空值,与substring()的“取”不同,tokeni…

    2025年12月17日
    000
  • XPath的error()函数怎么抛出错误?

    error()函数用于在XPath中主动抛出错误以中断执行,常用于数据验证、强制业务规则、调试及处理关键数据缺失等场景;在XSLT 3.0中可通过xsl:try/xsl:catch、在XQuery 3.0中通过try/catch机制捕获错误,并根据错误代码和描述进行日志记录或恢复处理;使用时应确保错…

    2025年12月17日
    000
  • XPath的remove()函数如何删除项?

    答案是XPath不提供删除功能,仅用于节点定位,删除需依赖宿主语言或工具实现。具体过程为:先用XPath表达式精准选择目标节点,再通过JavaScript的remove()、Python lxml库的remove()或XSLT转换等外部方法完成删除操作。这种设计体现了查询与操作的职责分离,确保XPa…

    2025年12月17日
    000
  • XPath的matches()函数支持正则表达式吗?

    是的,XPath的matches()函数支持正则表达式,这是XPath 2.0及以上版本引入的功能,用于实现比contains()更灵活的模式匹配。其语法为matches(input-string, pattern, flags?),可选标志包括i(不区分大小写)、m(多行模式)等。例如//div[…

    2025年12月17日
    000
  • XPath的environment-variable()函数怎么用?

    答案:XPath的environment-variable()函数用于在XPath 3.0+中读取系统环境变量,返回字符串值或空序列,适用于动态配置、环境适配、调试控制等场景,使用时需注意变量缺失、安全性、平台差异和执行环境限制,并可通过exists()判断或提供默认值来优雅处理空值。 XPath的…

    2025年12月17日
    000
  • XPath的lower-case()函数如何转换小写?

    lower-case()函数用于将字符串转为小写,语法为lower-case(string),支持非字符串参数的自动转换,适用于不区分大小写的匹配、数据标准化等场景,如//item/name/lower-case(.)返回小写名称,结合contains()可实现忽略大小写的搜索,空节点返回空字符串,…

    2025年12月17日
    000
  • XPath的ancestor轴如何选择祖先节点?

    ancestor轴用于向上追溯当前节点的所有祖先,从父节点直至根节点,支持通过节点类型和谓词条件(如属性、位置、内容)精准筛选目标祖先,常用于网页抓取中定位稳定容器、提取上下文信息或处理嵌套不规则的DOM结构。 XPath的 ancestor 轴,说白了,就是用来选定当前节点所有祖先的。它会从当前节…

    2025年12月17日
    000

发表回复

登录后才能评论
关注微信