Java JSON 序列化完全指南

Java 阅读约 14 分钟
JavaJSONJacksonGsonMoshi序列化

全面覆盖 Java 生态中 Jackson、Gson、Moshi 三大 JSON 库的用法、注解配置、性能对比以及 Spring Boot 集成的最佳实践。

Java JSON 生态概览

Java 的 JSON 处理库经历了早期的 org.json 和 json-lib,到 Jackson 凭借性能和功能优势成为事实标准,再到 Gson 和 Moshi 各自开辟差异化路线的演进过程。当前三大主流库各有其最适合的应用场景。

维护方核心优势适用场景
JacksonFasterXML功能最全、Spring Boot 默认、极快企业应用、微服务、复杂配置
GsonGoogleAPI 极简、零外部依赖、fromJson 强简单场景、快速原型、小工具
MoshiSquareKotlin 优先、类型安全、轻量级Android 开发、Kotlin 项目

Jackson 自 2.x 版本起已成为 Java 生态中无可争议的 JSON 处理标准。Spring Boot 的自动配置默认使用 Jackson,意味着大多数 Java 开发者其实已经在不知不觉中使用它。Gson 由 Google 维护,以其极其简洁的 API 著称,一行代码即可完成序列化。Moshi 由 Square 公司(OkHttp、Retrofit 的创造者)开发,专为 Kotlin 的空安全和不可变数据类设计,在 Android 生态中拥有很高的占有率。

Jackson ObjectMapper 基础

ObjectMapper 是 Jackson 的门面类,它负责协调序列化(write 系列方法)和反序列化(read 系列方法)的全过程。该类线程安全且创建成本高,应当作为全局单例在整个应用中复用。

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.databind.SerializationFeature;

ObjectMapper mapper = new ObjectMapper();
// 注册 Java 8 时间 API 模块 —— 否则 LocalDate 等类型无法处理
mapper.registerModule(new JavaTimeModule());
// 日期以 ISO 8601 字符串输出,而非时间戳数字
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

// 序列化
User user = new User("张三", 28, "zhangsan@example.com");
String json = mapper.writeValueAsString(user);
System.out.println(json);
// {"name":"张三","age":28,"email":"zhangsan@example.com"}

// 反序列化 —— 自动匹配字段名(默认大小写敏感)
User parsed = mapper.readValue(json, User.class);
assertEquals("张三", parsed.getName());

// 反序列化泛型集合 —— 必须用 TypeReference 保存类型信息
List<User> users = mapper.readValue(jsonArray,
    new TypeReference<List<User>>() {});

writeValueAsString 将对象转为字符串。对于直接写入文件或网络流,使用 writeValue(File/OutputStream, Object) 的流版本可以避免在内存中构建完整的字符串,在处理大对象时显著降低内存压力。

Jackson 核心注解详解

Jackson 提供了一套丰富的注解来控制序列化的行为细节,这些注解直接作用于模型类,实现了声明式的配置风格。

import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.time.LocalDateTime;

@JsonIgnoreProperties(ignoreUnknown = true)  // 忽略 JSON 中多余的未知字段
public class Article {

    @JsonProperty("article_id")   // 重命名为下划线风格
    private Long id;

    @JsonIgnore                    // 序列化和反序列化都忽略
    private String internalNote;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
    private LocalDateTime publishTime;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String subtitle;       // null 时不输出该字段

    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    private String computedField;  // 仅反序列化时可接受值,序列化时忽略

    @JsonSerialize(using = CustomAmountSerializer.class)
    private BigDecimal price;      // 使用自定义序列化器

    // getters / setters 省略
}

@JsonInclude 除了 NON_NULL,还支持 NON_EMPTY(空字符串、空集合也忽略)、NON_DEFAULT(值等于默认值也忽略)等策略。@JsonIgnoreProperties(ignoreUnknown = true) 加在类级别,对 API 版本向前兼容至关重要——服务端新增字段后旧版客户端不会因此报错。@JsonFormat 除了日期,还可以控制数字格式、枚举输出方式等。

Gson 的简洁之道

Gson 追求零配置开箱即用。和 Jackson 不同,Gson 默认访问所有字段(包括 private),不需要 getter/setter。

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.Since;

public class Config {
    @SerializedName("config_name")
    private String configName;

    @Expose(serialize = false, deserialize = true)
    private String secret;        // 只接受反序列化,不输出

    @Since(2.0)
    private String newField;      // 版本过滤:v2.0+ 才输出
}

// 构建定制化 Gson 实例(线程安全,可复用)
Gson gson = new GsonBuilder()
    .setPrettyPrinting()
    .setDateFormat("yyyy-MM-dd")
    .excludeFieldsWithoutExposeAnnotation()
    .setVersion(2.0)              // 启用 @Since 过滤
    .serializeNulls()             // 默认 Gson 不输出 null,此方法强制输出
    .create();

String json = gson.toJson(new Config(...));
Config parsed = gson.fromJson(json, Config.class);

Gson 默认不序列化 null 值,这是和 Jackson 的一大差异。excludeFieldsWithoutExposeAnnotation() 要求字段必须标记 @Expose 才会被处理,相当于引入了一个显式的白名单机制,适合对数据安全有严格要求的场景。

泛型类型擦除的应对方案

Java 泛型在编译后类型信息会被擦除,反序列化包含泛型参数的集合或包装类时必须额外传递类型标记。

// Jackson:使用 TypeReference 匿名子类捕获泛型信息
String listJson = "[{\"name\":\"张三\"},{\"name\":\"李四\"}]";
List<User> jacksonList = mapper.readValue(listJson,
    new TypeReference<List<User>>() {});

// Jackson:复杂泛型(如 Map<String, List<User>>)同样适用
String mapJson = "{\"groupA\":[{\"name\":\"张三\"}],\"groupB\":[{\"name\":\"李四\"}]}";
Map<String, List<User>> jacksonMap = mapper.readValue(mapJson,
    new TypeReference<Map<String, List<User>>>() {});

// Gson:使用 TypeToken 匿名类
java.lang.reflect.Type listType =
    new TypeToken<List<User>>(){}.getType();
List<User> gsonList = gson.fromJson(listJson, listType);

两种方案的原理相同:通过创建匿名子类,JVM 会在类的元数据中保留泛型参数的实际类型,库通过反射读取该信息以正确实例化目标类型。注意每次创建 TypeReference/TypeToken 匿名类都会在 JVM 中生成新的类字节码,虽然开销极小,但在超高频率调用时仍建议缓存 TypeReference 实例。

Jackson vs Gson vs Moshi 性能对比

实际性能差异在数据量大、调用频率高的情况下才具有参考意义。以下是基于 JMH 基准测试的典型数据。

// JMH Benchmark 结果概要(吞吐量越高越好,ops/s):
//
// 序列化(中等复杂度对象,1 万次迭代):
//   Jackson (2.15):   1,200,000 ops/s
//   Gson (2.10):        850,000 ops/s   (约 71%)
//   Moshi (1.14):     1,050,000 ops/s   (约 88%)
//
// 反序列化:
//   Jackson:          1,100,000 ops/s
//   Gson:               780,000 ops/s   (约 71%)
//   Moshi:            1,000,000 ops/s   (约 91%)
//
// 内存分配 (处理 1MB JSON):
//   Jackson:             2.1 MB
//   Gson:                2.8 MB
//   Moshi:               1.9 MB

Jackson 的性能优势源于多年的优化积累和流式 API 设计。Moshi 通过代码生成或注解处理器在编译时生成序列化适配器,避免了大量运行时反射。Gson 依赖于运行时反射,这在大型数据集上体现为可见的性能差距,但对于简单数据量小的 CRUD 应用这种差异通常可以忽略。

Spring Boot 中的 JSON 配置

Spring Boot 的自动配置使用 JacksonAutoConfiguration 来提供默认的 ObjectMapper Bean,开发者可以通过配置文件和 Bean 定制来覆盖默认行为。

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

@Configuration
public class JacksonConfig {

    @Bean
    public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
        return builder
            // 全局属性命名策略:驼峰 → 蛇形
            .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
            // 全局空值忽略
            .serializationInclusion(JsonInclude.Include.NON_NULL)
            // 遇到未知属性不报错
            .failOnUnknownProperties(false)
            // Java 8 日期时间支持
            .modules(new JavaTimeModule())
            .featuresToDisable(
                SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
                DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
            )
            .build();
    }
}

同时也可以使用 application.yml 配置常见属性,两者可以共存。

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Shanghai
    property-naming-strategy: SNAKE_CASE
    default-property-inclusion: non_null
    serialization:
      write-dates-as-timestamps: false

YAML 的配置简洁清晰,但能力有限:无法注册自定义 Module、无法配置 TypeReference 缓存等高级功能。实际项目中建议用 YAML 覆盖通用规则,用 @Bean 处理复杂需求,二者组合使用既能保持简洁又能处理高级场景。

常见陷阱汇总

日期格式、不可变对象、循环引用和泛型擦除是 Java JSON 开发中最常见的四个问题。

  • 日期格式:Jackson 默认将 Date 序列化为时间戳(长整型数字),必须禁用 WRITE_DATES_AS_TIMESTAMPS 才能输出 ISO 格式。LocalDateTime 需要 JavaTimeModule 才能序列化,否则直接报错。
  • 不可变对象:Java 14+ 引入的 record 是天然不可变的数据载体。Jackson 2.12+ 原生支持 record,Gson 需要自定义 InstanceCreator,Moshi 通过 @JsonClass(generateAdapter = true) 支持。
  • 循环引用:双向关联的实体类(如部门包含员工列表、员工又引用部门)在序列化时会导致无限递归。Jackson 通过 @JsonIdentityInfo 注解在第二次遇到同一对象时输出引用 ID 而非完整对象;Gson 没有内置解决方案,通常需要手动设计 DTO 切断引用链。
  • 泛型擦除:反序列化 List<User>Map<String, List<Item>> 等带泛型的容器时,必须使用 TypeReference(Jackson)或 TypeToken(Gson),否则会退化为 List<LinkedHashMap> 等原始类型。

各语言 JSON API 对比

语言 序列化 反序列化 美化输出 配置方式
Python json(内置) json.dumps(obj) json.loads(str) json.dumps(obj, indent=2) ensure_ascii, default, cls
JavaScript JSON(内置) JSON.stringify(obj) JSON.parse(str) JSON.stringify(obj, null, 2) replacer, space
Java Jackson / Gson mapper.writeValueAsString(obj) mapper.readValue(str, Class.class) mapper.writerWithDefaultPrettyPrinter() 注解、Module、Feature
Go encoding/json(内置) json.Marshal(obj) json.Unmarshal(data, &obj) json.MarshalIndent(obj, "", " ") struct tag
C# System.Text.Json(内置) JsonSerializer.Serialize(obj) JsonSerializer.Deserialize&lt;T&gt;(str) WriteIndented = true JsonSerializerOptions

常见问题 (FAQ)

JSON 序列化时如何处理日期时间类型?

大多数语言的 JSON 库默认不支持日期时间类型。通常做法是序列化为 ISO 8601 格式字符串(如 "2026-07-15T10:30:00Z")或 Unix 时间戳,反序列化时再转换回日期时间对象。Python 可通过 default 参数指定自定义编码器;JavaScript 可在 toJSON 方法中处理;Java 的 Jackson 支持 @JsonFormat 注解配置日期格式。

如何忽略 JSON 序列化中的 null 值或空字段?

不同语言的实现方式不同:Python 可在自定义编码器中过滤 None 值;JavaScript 可使用 replacer 函数或 JSON.stringify 的第二个参数;Java Jackson 用 @JsonInclude(Include.NON_NULL) 注解;Go 使用 omitempty struct tag;C# 设置 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull。

JSON 序列化时如何处理循环引用?

循环引用是 JSON 序列化的常见陷阱。标准 JSON 库通常会抛出异常或栈溢出。解决方案包括:使用 @JsonIdentityInfo 注解(Java Jackson)、实现自定义序列化器跳过反向引用、将对象图转换为 DTO 后再序列化,或使用专门的图序列化库如 flatted(JavaScript)。

为什么我的私有字段没有被序列化?

JSON 序列化库通常只序列化公开(public)字段或带有 getter 的属性。这是设计上的安全考虑。Python 的 json 模块默认只序列化 dict 的公共键;Java Jackson 可通过 @JsonProperty 注解私有字段或配置 ObjectMapper 的 Visibility;C# 中 System.Text.Json 默认只序列化公共属性,需通过 [JsonInclude] 特性包含非公共成员。

JSON 序列化性能有哪些优化技巧?

1) 复用序列化器实例(避免每次创建 ObjectMapper/JsonSerializerOptions);2) 对于大型数据,使用流式 API(如 Jackson 的 JsonGenerator / Python 的 iterencode);3) 使用编译时生成(C# Source Generator、Go easyjson);4) 避免过多嵌套层次;5) 数值类型避免不必要的大精度;6) 使用数据库分页或分块传输大 JSON。

推荐工具

以下工具可以帮助你在 Java 开发中更高效地处理 JSON 数据:

相关文章