Fastjson中的JSON.parseObject()详细讲解

前言

在 Java 开发尤其是 Web 后端开发中,我们每天都在和前后端的数据交互打交道。而这其中,出场率最高、也最容易让人踩坑的 API 之一,绝对有阿里巴巴 Fastjson 库中的 JSON.parseObject()

在使用 Fastjson(或其他 JSON 解析库)进行日常开发时,解析普通对象往往手到擒来。但一旦遇到多层嵌套的对象泛型集合,各种 ClassCastException 就会接踵而至。

很多开发者只知道去搜一行 TypeReference 的代码贴上去解决问题,却不明白底层到底发生了什么。今天,我们就用循序渐进的实战代码,结合底层 JVM 的“类型擦除”机制,彻底弄清楚JSON.parseObject().


一、JSON.parseObject()是什么

JSON.parseObject() 的本质是一个“极速翻译官”。

在互联网传输中,数据必须以**纯文本(字符串)**的形式流动,比如 {"name": "Alice", "age": 20}。但是,Java 是一门面向对象的强类型语言,它只认识 对象(Object)

JSON.parseObject() 的工作,就是读取这段字符串,然后在 Java 的内存中创造出一个对应的、活生生的 Java 对象,把数据严丝合缝地塞进去。

这个过程在计算机科学中被称为反序列化(Deserialization)

image-20260505214339555

1.0 json是什么

JSON本质就是一种“有结构的字符串格式”,用来表示数据。

1. 格式规范

  1. key 必须加双引号
1
2
{ "name": "Alice" } ✅
{ name: "Alice" } ❌
  1. 字符串必须用双引号
1
2
"name": "Alice" ✅
"name": 'Alice' ❌
  1. 不能有多余逗号
1
2
3
4
5
6
7
8
9
{
"name": "Alice",
"age": 20
} ✅

{
"name": "Alice",
"age": 20,
} ❌

2. JSON常见类型如下:

JSON 中 {} 表示对象(必须是键值对结构),[] 表示数组(可包含多个元素)。
如果需要表达多个对象,必须使用数组,而不能在 {} 中直接堆叠对象。

  1. 对象(Object)——用 {} 表示
  • 类似 Java 中的 Map<String, Object>

    1
    2
    3
    4
    5
    {
    "name": "Alice",
    "age": 20,
    "isStudent": true
    }
  • 特点:

    • {} 包裹

    • 数据是 键值对(key-value)

    • key 必须是字符串(必须加双引号)

  1. 数组(Array)——用 [] 表示
  • 类似 Java 中的 List

    1
    2
    3
    4
    5
    [
    "Apple",
    "Banana",
    "Orange"
    ]
  • 特点:

    • [] 包裹

    • 里面是有序数据集合

    • 每个元素可以是任意类型

  1. JSON 的嵌套结构
  • JSON 最大的特点是:可以无限嵌套

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    "name": "Agent",
    "actions": [
    {
    "actionName": "Click",
    "target": "ButtonA"
    },
    {
    "actionName": "Input",
    "target": "SearchBox"
    }
    ],
    "scores": {
    "confidence": 0.95
    }
    }

    这对应 Java 结构:

    1
    2
    3
    4
    5
    class Agent {
    String name;
    List<Action> actions;
    Map<String, Double> scores;
    }

1.1 转成通用的 JSONObject

不想写实体类,或者外部传来的 JSON 字段很不稳定,只需要提取其中一两个值的时候,用这种方式最合适。

深度理解: JSONObject 底层其实就是实现了 Map<String, Object> 接口。它相当于给你提供了一个万能容器,不管 JSON 长什么样都能装下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class Demo1 {
public static void main(String[] args) {
String jsonStr = "{\"orderId\":\"O_123\", \"amount\": 99.8, \"status\":\"SUCCESS\"}";

// 核心代码:直接转为 JSONObject
JSONObject jsonObj = JSON.parseObject(jsonStr);

// 像 Map 一样去取值
String orderId = jsonObj.getString("orderId");
Double amount = jsonObj.getDouble("amount");

System.out.println("订单号: " + orderId + ", 金额: " + amount);
}
}

1.2 转成自定义的 Java 实体类

这是企业级开发中最规范的做法。你需要明确指定一个“模具”(Class),让 Fastjson 按照这个模具去浇筑对象。

为 JSON 建立对应的 Java 实体类,有3个要点

  1. 必须保留公共无参构造函数(Fastjson 靠它通过反射实例化对象)。
  2. 嵌套类尽量使用 public static class(静态内部类),否则 Fastjson 无法独立实例化它。
  3. 必须提供标准的 Getter/Setter(可以使用 Lombok 的 @Data 注解)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class User {
private String name;
private Integer age;

// 省略 Getter 和 Setter 方法 (极其重要,后面会讲坑点)
}

public class Demo2 {
public static void main(String[] args) {
String jsonStr = "{\"name\": \"李白\", \"age\": 18}";

// 核心代码:传入字符串和目标类的 Class 对象
User user = JSON.parseObject(jsonStr, User.class);

System.out.println("用户名: " + user.getName());
}
}

1.3 JSON.parseObject 的反射原理解析

执行 JSON.parseObject(json, User.class) 时,Fastjson 底层借助java的反射机制实现json字符串和类对象的转换。原理如下:

  1. 词法解析 (Lexical Analysis - JSONLexer)

    • Fastjson 不会一开始就去管你的 Java 类。它首先像编译器一样,逐个字符读取 JSON 字符串,将其拆解成一个个 Token(标识符)。

    • 比如遇到 { 就知道是一个对象的开始,遇到 "name" 就知道是一个 Key。

  2. 获取元数据 (Metadata & JavaBeanInfo)

    • 解析器拿到目标类 User.class 后,会启动反射引擎

    • 它通过 Class.getDeclaredFields()Class.getMethods() 扫描这个类的所有结构。

    • 它会重点寻找:无参构造函数、所有的 Setter 方法。

    • 为了极致的性能,Fastjson 会把这些反射拿到的类结构信息缓存起来(存入 ParserConfig 的 ConcurrentHashMap 中),下次再解析同一个类时,直接从内存取,不再进行耗时的反射扫描。

  3. 实例化空对象 (Instantiation)

    • 通过反射拿到无参构造函数后,执行 constructor.newInstance()在内存中创造出一个全是 null 值的空对象
  4. 类型推导与反序列化 (Deserialization)

    • 读取 JSON 里的键值对。假设读到 "age": "18"

    • 通过前面缓存的元数据,发现 User 类里有一个 setAge(Integer age) 方法。

    • 此时触发类型转换 (TypeUtils):JSON 里是字符串 "18",目标方法要的是 IntegerFastjson 自动将其转换为整数 18

  5. 动态注入 (Method.invoke)

    • 最后一步,通过反射机制执行 method.invoke(userInstance, 18),把值硬塞进刚才创建的空对象里。整个对象拼装完成。

image-20260505214641133

1.4 经典问题:如何解析泛型集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String jsonArrayStr = "[{\"name\":\"李白\"}, {\"name\":\"杜甫\"}]";   // 一个列表
// User是一个类
// ❌ 错误做法:
// 1. JSON.parseObject不知道List列表中是什么对象
List<User> list = JSON.parseObject(jsonArray, List.class);
// 2.JSON.parseObject(..., User.class) 的返回值类型是 User,提示 Incompatible types(类型不兼容),代码根本跑不起来。
List<User> list = JSON.parseObject(jsonArray, User.class);

// 正确写法
// 1. 使用 parseArray,它专门用来对付以 [ 开头的数组
List<User> userList = JSON.parseArray(jsonArrayStr, User.class);
// 2. 使用TypeReference
Map<String, List<User>> complexData = JSON.parseObject(
"复杂的JSON串",
new TypeReference<Map<String, List<User>>>(){}
);

要用 parseObject 解析极其复杂的结构,必须加上大括号 {} 创建匿名内部类,把泛型写进去。

关于parseObject 解析极其复杂的结构,Fastjson 祭出了大杀器:TypeReference

既然运行时的对象记不住泛型,那我们就“现场造一个带泛型基因的新图纸”。

代码末尾的 {} 表示我们在现场创建了一个继承自 TypeReference 的匿名内部类。Java 规定,类的父类泛型信息会永久保留在字节码中。Fastjson 拿到这个匿名类后,顺藤摸瓜查它的“族谱”(父类),就能找回完整的 <List<AgentResponse>> 基因信息!

1.5 Java 的“类型擦除”

对于比较老的java版本,这个写法可以通过编译,但是依然没有实现json到类对象的转化,原因是java的类型”擦除”:

1
List<User> list = JSON.parseObject(jsonArray, List.class);

这里传给 Fastjson 的目标类型是 List.class,在运行时,泛型 <User> 被彻底擦除了,Fastjson 只知道你要一个 List,但不知道里面装什么,于是只能按最通用的 JSONObject 来兜底。

假设 Java 的世界是一个大型的物流中心。

泛型(比如 List<AgentResponse>)是什么?

它就像是贴在普通快递纸箱外面的一张便利贴,上面写着:“这里面只能装 AgentResponse!

  • 在发货前(写代码、编译时): Java 编译器(安检员)会死死盯着这张便利贴。如果你试图往纸箱里塞一个苹果(比如 String),安检员会立刻报错,阻止你发货。这就是泛型最大的作用:在写代码时保证类型安全。
  • 在运输途中(程序运行起来时,即 Runtime): Java 有一个极其坑爹的机制叫做“类型擦除”。为了省事和兼容老版本的 Java,纸箱一旦装上车(程序跑起来了),外面的那张便利贴就会被撕掉!

这就是真相:在程序真正运行的那一刻,所有的 List<AgentResponse>List<String>List<User>,在 JVM(Java虚拟机)眼里,全都变成了一个光秃秃的普通纸箱 —— 仅仅只是一个 List

Fastjson的设计流程如下:

  1. Fastjson 拿到了 JSON 字符串:[{"intent":"search"}]
  2. Fastjson 看到了你传进来的目标类型:List.class
  3. Fastjson 问 Java 虚拟机(JVM):“老哥,他让我解析成一个 List,但这纸箱里面到底该装什么类型的对象啊?”
  4. JVM 摊摊手:“不知道啊,便利贴(泛型)在程序一跑起来就被撕掉了(擦除了),我只知道它是个纸箱(List)。”
  5. Fastjson 无奈了:“既然不知道里面装啥,那我只能随便拿个最基础的容器给你兜着了。”于是,Fastjson 把 JSON 里的每一个 {} 都变成了通用的 JSONObject(相当于一个 Map),统统扔进了 List 里。

最终,你拿到的 list 里面,装的全是 JSONObject,而不是你想要的 User

image-20260505221728287**


二、代码实战

测试源码链接 :https://github.com/likerhood/CodeDesignWork/tree/main/codedesign0.0-0

2.1 准备测试数据

准备一份模拟外部 AI Agent 系统传来的 JSON 数据。

这是一个包含复杂嵌套(对象内部有 List 和 Map)的 JSON 数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[
{
"intent": "execute",
"actions": [
{"actionName": "Click", "target": "ButtonA"},
{"actionName": "Input", "target": "SearchBox"}
],
"confidenceScores": {
"execution_rate": 0.95
}
},
{
"intent": "summarize",
"actions": [
{"actionName": "Scroll", "target": "PageBottom"}
],
"confidenceScores": {
"summary_rate": 0.88
}
}
]

2.2 实体类定义

我们需要为上面的 JSON 建立对应的 Java 实体类,注意以下三点:

  1. 必须保留公共无参构造函数
  2. 嵌套类尽量使用 public static class(静态内部类)
  3. 必须提供标准的 Getter/Setter(这里为了代码直观,使用了 Lombok 的 @Data 注解)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import lombok.Data;
import java.util.List;
import java.util.Map;

@Data
public class AgentResponse {
private String intent;

// 重点关注:这是一个包含泛型的集合属性
private List<Action> actions;
private Map<String, Double> confidenceScores;

@Data
public static class Action {
private String actionName;
private String target;
}
}

2.3 解析实体类内部的泛型集合

疑惑点: Java 运行时会发生“类型擦除”,那 AgentResponse 内部的 List<Action> 里的泛型 <Action> 会被擦除吗?Fastjson 会不会解析失败?

结论: 不会!完美解析。

底层原理: Java 的类型擦除只擦除内存中实例对象的泛型,绝对不会擦除类结构声明上的泛型.编译器会把 List<Action> 这个泛型签名死死地烙印在 .class 字节码文件中。Fastjson 底层通过 Field.getGenericType() ,查阅字节码文件,就能精准得知必须装入 Action 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.alibaba.fastjson.JSON;
// 1. 读取本地 test.json 文件(包含多个 Agent 响应的数组)
String jsonStr = new String(Files.readAllBytes(Paths.get("test.json")));

// 2. 先解析为 List 集合
List<AgentResponse> list = JSON.parseObject(jsonStr, new TypeReference<List<AgentResponse>>(){});

// 3. 取出第一个对象来验证“内部嵌套泛型”
AgentResponse response = list.get(0);

// 4. 核心验证:测试能否正常拿取内部嵌套的 Action 对象
// 如果泛型被擦除,这里拿到的会是 JSONObject,调用 getActionName() 会直接报错
System.out.println("外层意图: " + response.getIntent());
System.out.println("内层第一个动作名: " + response.getActions().get(0).getActionName());
System.out.println("内层第一个动作目标: " + response.getActions().get(0).getTarget());

运行结果:成功输出,没有任何报错。

2.4 直接解析顶层匿名泛型集合

💡 疑惑点: 如果我们直接把刚才那个整体的 JSON 数组传进去,并试图用 List.class 去接,会发生什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.alibaba.fastjson.JSON;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

public class DemoTrap {
public static void main(String[] args) throws Exception {
// 1. 读取我们的 test.json 文件(一个完整的 [] 数组字符串)
String jsonArrayString = new String(Files.readAllBytes(Paths.get("test.json")));

// 2. ❌ 致命错误示范:直接用 List.class 去接
List<AgentResponse> list = JSON.parseObject(jsonArrayString, List.class);

// 3. 表面上看没有报错,它甚至能打印出正确的大小!
System.out.println("集合大小: " + list.size());

// 4. 🚨 运行时爆炸:这一步必定抛出 ClassCastException 异常!
AgentResponse firstResponse = list.get(0);
}
}

原因: 当我们在方法参数中传递 List.class 时,程序已经运行起来了。此时,JVM 遵循“类型擦除”,把泛型便利贴无情撕掉。Fastjson 问 JVM:“这个 List 里面装什么?” JVM 答:“不知道,就个普通 List。”

既然不知道,Fastjson 只能用最基础的通用字典 JSONObject 来兜底。所以,list.get(0) 拿到的根本不是 AgentResponse,而是一个 JSONObject!强转必定报错。


2.5 TypeReference 终极破解

代码末尾的 {} 表示我们在现场创建了一个继承自 TypeReference 的匿名内部类

Java 规定,类的父类泛型信息会永久保留在字节码中。

Fastjson 拿到这个匿名类后,顺藤摸瓜查它的“族谱”(父类),就能找回完整的 <List<AgentResponse>> 基因信息!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

public class DemoSuccess {
public static void main(String[] args) throws Exception {
// 1. 读取我们一开始准备的 JSON 测试文件
String jsonStr = new String(Files.readAllBytes(Paths.get("agent_data.json")));

// 2. ✅ 正确示范:使用带大括号 {} 的 TypeReference 锁死完整的泛型树
List<AgentResponse> list = JSON.parseObject(
jsonStr,
new TypeReference<List<AgentResponse>>() {}
);

// 3. 验证成果
for (AgentResponse agent : list) {
System.out.println("----------");
System.out.println("Agent 意图: " + agent.getIntent());
System.out.println("第一个动作目标: " + agent.getActions().get(0).getTarget());
System.out.println("执行置信度: " + agent.getConfidenceScores());
}
}
}

总结

JSON.parseObject() 本质是通过 反射 + 类型推导 将 JSON 字符串还原为 Java 对象。

在实际开发中,问题的核心并不在 API 本身,而在于 Java 的类型机制

  • 普通对象解析:依赖无参构造 + Setter,按字段映射即可完成
  • 类内部泛型:泛型信息保存在字节码中,Fastjson 可通过反射获取,解析无压力
  • 顶层泛型集合:由于类型擦除,运行时丢失泛型信息,必须借助 TypeReference 补全类型

👉 一句话记住:

能解析 ≠ 写对了,关键在于“类型信息是否在运行时可获取”