☰
Android Gson解析实战:从JSON对象到多层嵌套数组
2026/10/15 13:28:07 网站建设 项目流程

1. 为什么 Gson 值得单独花一讲来学

做 Android 开发,绕不开数据交换。不管是请求后端接口拿用户信息,还是读取本地配置文件,数据几乎都以 JSON 的形式存在。Android 原生其实自带了一套org.json包,能解析 JSON,但用过的都知道,那玩意儿写起来又臭又长:一层一层getJSONObject、getString,稍微嵌套深一点,代码就变成“套娃”,可读性极差,还特别容易写错 key 的名字。

Gson 是 Google 开源的一个 Java 库,专门用来在 Java 对象和 JSON 字符串之间做双向转换。它的核心价值就一句话:把 JSON 字符串直接映射成 Java 对象,反过来也能把 Java 对象直接序列化成 JSON 字符串。你不需要手动去解析每一个字段,只要定义好一个和 JSON 结构对应的实体类,剩下的交给 Gson 就行。

这一讲我打算把 Gson 从“能用”讲到“用明白”。内容包括:Gson 的基本用法、JSON 对象解析、对象嵌套、数组、对象数组,以及最让人头疼的 JSON 对象嵌套 JSON 对象数组这种多层结构。每一块我都会给出完整的实体类定义和解析代码,并且解释为什么这么写。适合刚入门 Android、已经会写基本 Activity、但对网络数据和 JSON 处理还比较模糊的同学。如果你已经用过 Gson 但总是靠“试”来写实体类,那这篇也能帮你把逻辑理顺。

提示:本文所有代码基于 Gson 2.10 以上版本,Android 项目通过 Gradle 引入即可,不需要额外配置。

2. Gson 的引入与最简上手

2.1 依赖引入与版本选择

在 Android 项目里用 Gson,第一步是在模块级的build.gradle里加依赖。现在主流是 Gradle Kotlin DSL,写法如下:

dependencies { implementation("com.google.code.gson:gson:2.10.1") }

如果你用的是 Groovy DSL,就是implementation 'com.google.code.gson:gson:2.10.1'。版本号我建议用 2.10.1 或更高,早期 2.8.x 在处理某些泛型和空值时有一些已知问题,新版本更稳。

这里有个细节值得说:Gson 是一个纯 Java 库,体量很小,大概 200 多 KB,不会明显增加 APK 体积,所以不需要纠结“要不要用第三方库”这个问题。它不像某些重型框架会拖慢编译,引入成本几乎可以忽略。

2.2 两个核心方法:toJson 和 fromJson

Gson 的 API 设计非常克制,最常用的就两个方法:

  • toJson(Object):把 Java 对象转成 JSON 字符串,叫序列化。
  • fromJson(String, Class):把 JSON 字符串转成 Java 对象,叫反序列化。

先看一个最简单的例子。假设有一个用户类:

public class User { private String name; private int age; // 省略 getter/setter 和构造方法 }

解析代码只有两行:

Gson gson = new Gson(); User user = gson.fromJson(jsonString, User.class);

反过来:

String json = gson.toJson(user);

就这么简单。Gson 会自动根据字段名去匹配 JSON 里的 key。字段名是name,它就找 JSON 里的name;字段名是age,就找age。匹配不上就留空,不会报错。

注意:Gson 默认使用字段名做映射,而不是 getter/setter 方法名。这一点和某些框架不同,别搞混了。

2.3 实体类字段名与 JSON key 不一致怎么办

实际开发中经常遇到后端返回的 key 是下划线风格,比如user_name,而 Java 字段习惯用驼峰userName。这时候有两种处理方式。

第一种是用@SerializedName注解:

public class User { @SerializedName("user_name") private String userName; @SerializedName("user_age") private int userAge; }

这个注解是 Gson 最常用的注解之一,作用是告诉 Gson:JSON 里叫user_name的字段,映射到我这个userName属性上。序列化和反序列化都会遵守这个映射。

第二种是配置字段命名策略:

Gson gson = new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .create();

这样所有驼峰字段在转 JSON 时都会自动变成下划线风格。但这种方式是全局的,如果只有个别字段不一致,还是用@SerializedName更精准。我个人的习惯是:接口字段风格统一时用命名策略,零散不一致时用注解。

3. 逐层拆解:从 JSON 对象到多层嵌套

3.1 解析单个 JSON 对象

单个 JSON 对象是最基础的结构,形如:

{ "name": "张三", "age": 25, "email": "zhangsan@example.com" }

对应的实体类就是三个字段,字段名和 key 保持一致即可。解析时直接gson.fromJson(json, User.class)。

这里有个新手常踩的坑:实体类的字段类型必须和 JSON 值的类型兼容。比如 JSON 里age是数字25,实体类写成String age,Gson 也能解析,它会做类型转换,把数字转成字符串。但反过来,如果 JSON 里是字符串"25",实体类写成int age,Gson 同样能转。这种“宽容”有时候是好事,有时候会掩盖问题。我的建议是类型尽量对齐,别依赖 Gson 的自动转换,否则后端字段类型一变,你这边可能悄悄出问题。

另外,实体类一定要有无参构造方法。Gson 底层是通过反射创建对象的,如果没有无参构造,某些场景下会报错。虽然 Gson 也支持通过Unsafe绕过构造方法,但那是内部机制,不要依赖。老老实实写个无参构造,省心。

3.2 对象嵌套:JSON 里套 JSON

嵌套结构在实际接口里非常普遍,比如:

{ "name": "张三", "age": 25, "address": { "city": "北京", "street": "朝阳路", "zipCode": "100000" } }

这时候实体类也要嵌套:

public class User { private String name; private int age; private Address address; } public class Address { private String city; private String street; private String zipCode; }

解析代码不变,还是gson.fromJson(json, User.class)。Gson 会自动递归地把address这个 JSON 对象转成Address实例。

关键点在于:嵌套的层级要和实体类的层级一一对应。JSON 里address是一个对象,实体类里address字段的类型就必须是一个类,而不是String或Map。很多新手看到嵌套就慌,其实只要把每一层都定义成一个类,结构就清晰了。

我一般画实体类的习惯是:先看 JSON 最外层有哪些 key,每个 key 对应一个字段;如果某个 key 的值是{},就再建一个类;如果是[],就用List。这样一层层剥下去,再复杂的结构也能拆明白。

3.3 解析 JSON 数组

JSON 数组形如:

[ {"name": "张三", "age": 25}, {"name": "李四", "age": 30} ]

解析数组时,fromJson的第二个参数不能直接传User.class,因为数组不是单个对象。有两种写法。

第一种用数组类型:

User[] users = gson.fromJson(json, User[].class);

第二种用TypeToken获取泛型类型:

Type listType = new TypeToken<List<User>>(){}.getType(); List<User> users = gson.fromJson(json, listType);

我更推荐第二种,因为List比数组好用,增删改查都方便。TypeToken这个写法看起来有点怪,new TypeToken<List<User>>(){}后面带个花括号,这是匿名内部类的语法,目的是在运行时保留泛型信息。Java 的泛型在编译后会被擦除,直接写List<User>.class是不合法的,所以 Gson 用TypeToken这个技巧来“记住”泛型。

注意:TypeToken的花括号不能省,写成new TypeToken<List<User>>()是错的,编译不过。

3.4 对象数组:数组里装的是对象

对象数组其实就是数组的元素是对象,和上面 3.3 说的是同一回事。但这里要强调一个容易混淆的点:JSON 数组和 Java 数组/List 的对应关系。

  • JSON[]空数组 → JavaList空集合,不是null。
  • JSON 数组里每个元素是对象 → JavaList<对应类>。
  • JSON 数组里每个元素是字符串 → JavaList<String>。

我见过有同学把List<User>写成List<Object>,结果解析出来一堆LinkedTreeMap,取数据时各种强转,非常痛苦。根因就是泛型没写对。Gson 在泛型信息缺失时,会退化成用LinkedTreeMap来存对象,这是它的兜底策略。所以泛型一定要写具体。

3.5 重头戏:JSON 对象嵌套 JSON 对象数组

这是实际接口里最常见也最容易写错的结构。看一个典型例子:

{ "code": 200, "message": "success", "data": { "total": 2, "users": [ { "name": "张三", "age": 25, "tags": ["VIP", "活跃"] }, { "name": "李四", "age": 30, "tags": ["新用户"] } ] } }

这个结构是:最外层是对象,里面data是对象,data里users是对象数组,每个 user 里tags又是字符串数组。四层结构,层层嵌套。

拆解思路还是老规矩,从外往里:

public class ApiResponse { private int code; private String message; private Data data; } public class Data { private int total; private List<User> users; } public class User { private String name; private int age; private List<String> tags; }

解析代码:

ApiResponse response = gson.fromJson(json, ApiResponse.class); List<User> users = response.getData().getUsers();

看起来复杂,其实只要把每一层对应好,代码非常干净。这就是 Gson 相比手动解析最大的优势:结构即代码,代码即结构。

我再补充一个更刁钻的场景:如果data里的users有时候是数组,有时候是空对象{},怎么办?这种“脏数据”在实际接口里偶尔会遇到。Gson 默认会抛异常。解决办法是自定义JsonDeserializer,在反序列化时判断类型。这个属于进阶内容,但思路要清楚:Gson 的扩展点就是JsonDeserializer和JsonSerializer,遇到默认行为处理不了的情况,就注册自定义处理器。

4. 实操全流程:从拿到 JSON 到取出数据

4.1 完整实操步骤

我把整个流程拆成可复制的步骤,你照着做就行。

第一步,拿到 JSON 字符串。通常来自网络请求的响应体,或者本地文件。假设字符串存在变量json里。

第二步,分析 JSON 结构。用任意 JSON 格式化工具把字符串展开,看清楚每一层的类型:是对象{}还是数组[]。

第三步,定义实体类。从最外层开始,遇到对象建类,遇到数组用List。字段名和 key 对齐,不一致的用@SerializedName。

第四步,创建 Gson 实例。简单场景用new Gson(),需要配置的用GsonBuilder。

第五步,调用fromJson解析。数组和泛型用TypeToken。

第六步,从对象里取数据,做业务处理。

4.2 参数与配置的选择过程

GsonBuilder有几个常用配置,我逐个说下什么时候用。

setDateFormat("yyyy-MM-dd HH:mm:ss"):当 JSON 里的日期是字符串,实体类字段是Date类型时,必须配置这个,否则 Gson 默认的日期解析格式可能对不上,会抛异常。

serializeNulls():默认情况下,Gson 序列化时会忽略值为null的字段。如果你希望null也输出成"field": null,就加上这个。一般接口对接时,后端可能要求字段必须存在,这时候有用。

setPrettyPrinting():让输出的 JSON 带缩进,方便调试查看。生产环境序列化时不要开,会增加体积。

disableHtmlEscaping():Gson 默认会把<、>、&等字符转义成 Unicode,如果 JSON 里要传 HTML 片段,这个转义会导致内容不对。加上这个配置可以关闭转义。

我的习惯是封装一个工具类,把这些配置集中管理:

public class GsonUtil { private static final Gson GSON = new GsonBuilder() .setDateFormat("yyyy-MM-dd HH:mm:ss") .disableHtmlEscaping() .create(); public static <T> T parse(String json, Class<T> clazz) { return GSON.fromJson(json, clazz); } public static <T> T parse(String json, Type type) { return GSON.fromJson(json, type); } public static String toJson(Object obj) { return GSON.toJson(obj); } }

这样全局统一配置,避免每个地方new Gson()导致行为不一致。

4.3 实操现场记录:一个嵌套数组的完整解析

我拿 3.5 那个结构走一遍完整代码,你可以直接复制到项目里改。

String json = "{\"code\":200,\"message\":\"success\",\"data\":{\"total\":2,\"users\":[{\"name\":\"张三\",\"age\":25,\"tags\":[\"VIP\",\"活跃\"]},{\"name\":\"李四\",\"age\":30,\"tags\":[\"新用户\"]}]}}"; Gson gson = new Gson(); ApiResponse response = gson.fromJson(json, ApiResponse.class); if (response.getCode() == 200) { List<User> users = response.getData().getUsers(); for (User user : users) { Log.d("GsonDemo", "姓名:" + user.getName() + ",年龄:" + user.getAge()); for (String tag : user.getTags()) { Log.d("GsonDemo", "标签:" + tag); } } }

运行后日志会依次打印出张三、李四的信息和各自的标签。整个过程没有任何手动解析 JSON 的代码,全靠实体类映射。

这里我要强调一个实操心得:实体类的 getter 方法一定要写全。Gson 反序列化靠反射写字段,不需要 getter;但你取数据时要用 getter,所以别偷懒。我见过有人只写字段不写 getter,结果解析完取不到值,排查半天。

5. 常见问题与排查技巧实录

5.1 解析结果为 null 或字段为空

这是最高频的问题。原因通常有三个。

第一,字段名不匹配。JSON 里是user_name,实体类写userName,又没加@SerializedName,Gson 找不到对应关系,字段就是默认值。排查方法:打印 JSON 的 key,和实体类字段逐个对照。

第二,实体类没有无参构造。Gson 反射创建对象时失败,可能返回 null 或抛异常。加上无参构造即可。

第三,泛型没写具体。比如List<Object>会导致解析成LinkedTreeMap,取字段时类型不对。检查TypeToken里的泛型是否精确。

5.2 报错 JsonSyntaxException

这个异常说明 JSON 格式本身有问题,比如多了逗号、少了引号、括号不匹配。排查方法:把 JSON 字符串复制到在线格式化工具里校验,能格式化成功说明格式没问题,格式化失败就是 JSON 本身有语法错误。

还有一种情况是 JSON 里某个字段的值类型和实体类差异太大,比如 JSON 是对象{},实体类字段是String,Gson 无法转换,也会抛这个异常。这时候要么改实体类类型,要么写自定义JsonDeserializer。

5.3 日期解析失败

JSON 里的日期是"2024-01-01 12:00:00",实体类字段是Date,但没配置setDateFormat,Gson 会用默认格式解析,大概率失败。解决办法就是加上setDateFormat("yyyy-MM-dd HH:mm:ss"),格式字符串要和 JSON 里的实际格式完全一致。

如果日期格式不固定,建议实体类字段直接用String,取出来自己解析,别让 Gson 处理,省得踩坑。

5.4 常见问题速查表

问题现象可能原因解决办法
字段值为 null字段名不匹配加@SerializedName或统一命名策略
解析抛异常JSON 格式错误用格式化工具校验 JSON
取数据强转失败泛型写成 Object泛型写具体类型,用 TypeToken
日期解析失败未配置日期格式setDateFormat或字段用 String
嵌套对象为空实体类层级不对应逐层检查实体类结构
数组解析成 Map泛型信息丢失用TypeToken<List<T>>

5.5 独家避坑技巧

第一个技巧:用@SerializedName的alternate属性处理字段名兼容。比如后端有时候返回userName,有时候返回user_name,可以这样写:

@SerializedName(value = "userName", alternate = {"user_name", "username"}) private String userName;

这样三种 key 都能映射到同一个字段,接口字段改名时不用改代码。

第二个技巧:实体类字段用包装类型而不是基本类型。比如Integer而不是int。因为 JSON 里字段可能缺失,用int会默认成 0,你分不清是“真的是 0”还是“没解析到”。用Integer缺失时是null,能明确判断。

第三个技巧:解析前先判空。gson.fromJson(null, User.class)返回 null,不会抛异常,但后续用这个对象就会空指针。养成习惯,解析后先判空再使用。

第四个技巧:大 JSON 用流式解析。如果 JSON 特别大,比如几 MB,fromJson会一次性加载到内存,可能 OOM。这时候用JsonReader流式读取,边读边处理。不过一般接口返回的数据量不会这么大,知道有这个方案即可。

6. Gson 与其他方案的取舍思考

6.1 和原生 org.json 的对比

原生org.json的优势是零依赖,Android 自带。但它的 API 是命令式的,解析嵌套结构时代码量成倍增长。举个例子,解析 3.5 那个结构,用原生写法大概要写二三十行,而 Gson 只要定义三个类加一行解析。可维护性差距巨大。

Gson 的劣势是增加了一个依赖,以及需要定义实体类。但对于任何有一定规模的项目,实体类带来的结构清晰度远超这点成本。我的判断标准是:只要 JSON 结构超过一层,就用 Gson。

6.2 和手动拼接字符串的对比

有些同学序列化时喜欢手动拼 JSON 字符串,比如"{\"name\":\"" + name + "\"}"。这种做法在字段少的时候看似简单,但一旦字段里有引号、换行、特殊字符,就会拼出非法 JSON。Gson 的toJson会自动处理转义,安全得多。所以序列化也建议用 Gson,别手动拼。

6.3 什么场景不适合用 Gson

Gson 也不是万能的。如果 JSON 结构极其动态,字段名和数量都不固定,比如某些配置类数据,用Map<String, Object>配合 Gson 解析反而更灵活,不必强行定义实体类。另外,如果对性能有极致要求,比如每秒解析上万条数据,可以考虑更轻量的方案,但对绝大多数 App 来说,Gson 的性能完全够用。

我在实际项目里的体会是,Gson 最大的价值不是“快”,而是“稳”和“清晰”。实体类一旦定义好,整个数据流就是类型安全的,编译期就能发现很多问题,而不是等到运行时才报错。这一点在团队协作里尤其重要,别人看你的实体类就知道接口返回什么结构,比看一堆getString强太多。

最后分享一个小技巧:如果你不确定实体类写得对不对,可以先写个反向测试——用gson.toJson(你的对象)生成 JSON,再和接口返回的 JSON 对比结构。如果结构一致,说明实体类定义正确。这个“自检”方法我用了很多次,比反复调试解析代码高效得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询