JSON 工具箱

JSON 转 Java 类:生成器只给骨架,生产还要补的五件事

结论:用生成器实测 JSON → Java 的默认输出——number 映射成 Double、嵌套对象变 static 内部类、不生成任何注解。对照三路英文圈的实战经验,给出金额精度、字段改名、泛型擦除、未知字段、日期与无参构造五个必须手工补齐的位置与可复制代码。

进阶约 10 分钟发布于 2026-11-04更新于 2026-11-04
把 JSON 生成 Java POJO 类代码的转换工具界面封面

结论:生成器十秒钟给出的类结构可以直接进编译,但不能直接进生产。本站生成器的实测输出是 number→Double、嵌套对象→static 内部类、不生成任何 Jackson 注解——金额精度、字段改名、泛型擦除、未知字段、日期这五件事,它一件都不会替你做。

把一份接口响应变成 Java 类是纯机械劳动:看着 JSON 抄字段,套上 getter/setter。机械劳动的代价是那把看不见的刀——属性名抄错一个字母不会编译报错,字段只会静默地变成 null,等你到生产日志里才发现。生成类结构就是为了从第一天对齐名字和类型,但生成器能对齐的只有「从样本里看得见」的部分。

一、先把生成器跑一遍:它到底给了什么

拿一份有代表性的接口响应,粘进本站的 JSON 转 Java 工具页:

{
  "order_id": 202611040001,
  "buyer": {
    "name": "王五",
    "vip": true
  },
  "items": [
    { "sku": "A-1", "price": 19.99, "qty": 2 }
  ],
  "created_at": "2026-11-04T09:30:00+08:00"
}

生成器输出的骨架(默认选项,useLombok 关闭时):

public class Root {
    private Double orderId;
    private Buyer buyer;
    private List<Item> items;
    private String createdAt;

    public Double getOrderId() { return orderId; }
    public void setOrderId(Double orderId) { this.orderId = orderId; }
    // ...其余 getter/setter 同理
}

Buyer 与 Item 是单独的 static 内部类。逐条对照映射规则:

JSON 里的形态 生成的 Java 类型 备注
"name": "王五" String 字符串恒为 String
"qty": 2 Double 所有数字都是 Double,见第二节
"vip": true Boolean 包装类型,允许 null
"buyer": {...} Buyer(static 内部类) 嵌套对象自成一类
"items": [...] List<Item> 元素类型取自首元素
order_id orderId 蛇形命名自动转驼峰

类名走 PascalCase(Root),字段走 camelCase(orderId),撞上 Java 关键字(class、new、int)时自动加后缀避开。可选参数三个:packageName 包名、className 根类名、useLombok 生成 @Data 还是手写 getter/setter。开启 Lombok 后整段样板代码收敛成几个注解。

这份骨架有两个边界要心里有数。第一,它不生成任何 Jackson/Gson 注解——@JsonProperty、@JsonIgnoreProperties、@JsonFormat 一概没有,因为注解决定的是「反序列化怎么执行」,而样本数据推不出执行策略。第二,它没有反向转换:从 Java 类反推 JSON 不在能力范围内,这与JSON 转 TypeScript Interface那条前向路径是同一种单向设计。

二、第一件要补的事:Double 不是金额该有的类型

生成器把 "price": 19.99 映射成 Double,编译通过、赋值正常,但这个类型不该接收钱。二进制浮点数表示不了 0.1,也就表示不了大多数十进制小数:

System.out.println(19.99);          // 19.99,看起来没事
double a = 0.1 + 0.2;
System.out.println(a);              // 0.30000000000000004
System.out.println(a == 0.3);       // false

单独打印 19.99 时 Java 会做最短表示美化,看不出问题;一旦进入运算,误差立刻现形。三路英文圈的教程在这一点上完全一致:价格、金额、费率一律改成 BigDecimal,并把构造方式定为 new BigDecimal("19.99")(字符串构造),而不是 new BigDecimal(19.99)——后者会把 double 的误差原样带进来。

机械地补法是生成后全局替换:Double → BigDecimal(仅限金额字段)。顺带说一句同源问题:超过 2^53 的整数(订单号、雪花 ID)如果走 Double,末几位会被静默舍入,20 位数字掉三位末尾是实测会发生的,机制与JSON 大整数为什么会丢精度是同一个。这类字段改成 String 或 Long,让数字以文本形式进出。

字段性质 生成器给的 应该改成
金额、费率 Double BigDecimal(字符串构造)
订单号、ID(可能超 2^53) Double String 或 Long
数量、页码这类整数 Double Integer / Long
真正参与科学计算的浮点 Double 保留

三、第二件:字段名对不上,靠 @JsonProperty

生成器把 order_id 转成 orderId 是「善意推测」:它假设你希望 Java 侧是驼峰。但 Jackson 默认按字段名逐字匹配 JSON 键,两边对不上时不会报错,orderId 只会是 null。两种修法:

字段级——加 @JsonProperty,一个注解钉死一个映射:

public class Root {
    @JsonProperty("order_id")
    private Long orderId;
}

全局级——ObjectMapper 换命名策略,整个项目不用逐字段写注解:

ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);

选哪条取决于对接面:只对接一个固定 API 用字段级更清晰;整个团队都对蛇形命名的外部服务时用全局策略。这里有一个值得单独强调的反面事实——名字写错的失败模式是「静默 null」而不是异常。jsonformater 的工具页把这一点当作生成器存在的理由:机械抄写正是在这一步出错率最高。所以补完注解后,至少写一个断言测试确认关键字段非 null。

四、第三件:泛型擦除,List 别裸着反序列化

生成器给了你 List<Item> 的字段声明,但真正把 JSON 数组塞进这个 List 的是 Jackson。在顶层直接这样写会掉进泛型擦除的坑:

// 错误:运行时拿到的元素是 LinkedHashMap
List<Item> items = mapper.readValue(json, List.class);

// 正确:TypeReference 保留泛型参数
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});

readValue(json, List.class) 能编译能运行,但 Java 的泛型只在编译期存在,Jackson 运行时只看到 List,元素只好落成 LinkedHashMap。错误不会立刻爆发,等到某处 item.getSku() 才抛 ClassCastException,报错位置离出错位置隔了十万八千里。三种正确写法按场景选:

// 1. 顶层就是数组
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});

// 2. 顶层对象的某个字段
JsonNode root = mapper.readTree(json);
List<Item> items = mapper.convertValue(root.get("items"), new TypeReference<List<Item>>() {});

// 3. JavaType 显式构造
JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, Item.class);
List<Item> items = mapper.readValue(json, type);

Gson 侧同理用 TypeToken。这是三路教程都单独立条的坑,因为它失败得晚、报得远。

五、第四件:未知字段,别让 API 演进弄坏反序列化

接口加一个新字段,是再正常不过的演进。Jackson 的默认行为却是对未知字段抛 UnrecognizedPropertyException——对端加了字段,你的服务直接开始报错。两个层级关掉它:

// 类级:只容忍这个类的未知字段
@JsonIgnoreProperties(ignoreUnknown = true)
public class Root { ... }

// 全局:整个 mapper 都容忍
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

关掉之后还有一个镜像问题:容错意味着字段名拼错也不报错了,上一节那个「静默 null」的失败模式从此畅通无阻。折中做法是生产环境关容忍、测试环境开着跑一遍核心接口,让拼写错误在测试期暴露。Gson 默认就是忽略未知字段的,这也是它在快速对接场景里省心的原因——省心的代价是你永远不知道对端加了什么。

六、第五件:日期与无参构造

生成器把 created_at 映射成 String,这其实是最稳妥的默认:JSON 里日期就是字符串,格式有 ISO-8601、Unix 时间戳、自定义三态,样本推不出你想要哪种。需要真正的日期类型时补两条:

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
// 序列化时输出 ISO-8601 而不是时间戳数组
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
public class Root {
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createdAt;
}

无参构造是另一个必查项:Jackson 反序列化靠无参构造创建对象再逐字段 set。你手写了带参构造器,默认构造器就消失了,反序列化立刻失败。Lombok 开启时补 @NoArgsConstructor;用 Java 17 的 record 则直接交给 Jackson 2.12+ 的 record 支持,靠构造器参数匹配,无参构造这条要求就没了。

七、生产两件事:ObjectMapper 复用与入参校验

生成器给的是类,运行时还有两件架构层面的事。第一,ObjectMapper 创建成本高且线程安全,全应用共享一个实例——在方法里 new ObjectMapper() 是最常见的性能反模式,Spring Boot 默认注入的那个就是为此存在。全局命名策略、JavaTimeModule、未知字段策略都配在这一个实例上,别散落各处。

第二,反序列化成功只说明类型对得上,不说明值合法。价格是负数、数量为 0、邮箱格式不对,Jackson 一概不管。入参校验交给 Bean Validation(hibernate-validator),在生成骨架上补注解即可:

public class CreateOrderRequest {
    @NotNull
    @DecimalMin(value = "0.01")
    private BigDecimal price;

    @Min(1)
    private Integer qty;

    @Email
    private String contact;
}

Spring 里配 @Valid 即在进入业务逻辑前拦截。这一层是生成器永远不会给你的——它只能从样本看出「字段长什么样」,看不出「这个字段的业务约束是什么」。

八、生成与手写的边界

什么时候用生成器,什么时候手写,边界其实很清楚:

  • 用生成器:字段多的接口响应、一次性对接的第三方 API、要快速出原型的场景。它消灭的是「机械抄写出错」这一种错误,十秒钟拿到对齐了名字和类型的骨架。
  • 手写或深度改造:金额与长整数要改类型、命名策略要全局定、核心领域对象要加校验注解。这几件是业务决策,样本数据推不出来。
  • 两边的共同前提:先确认源 JSON 本身合法。生成器拿到残缺输入照样生成,坑留给了反序列化那一步;粘进工具页前用本站校验页过一遍,十秒钟的事。

生成器解决的是「从 0 到骨架」,本文五件事解决的是「从骨架到生产」。次序不能反,也不能省。

常见问题

生成器输出的类能直接用于 Spring Boot 吗?

编译期可以,运行期建议先补三件事:金额字段改 BigDecimal、对接蛇形命名的 API 时定命名策略或加 @JsonProperty、开 @JsonIgnoreProperties(ignoreUnknown = true) 防 API 演进。Bean Validation 注解按业务补。

为什么生成器把数字都映射成 Double,不是 long 或 BigDecimal?

样本数据只能区分「整数还是小数」,区分不了业务语义。一个 2 可能是数量也可能是编号,20 位订单号和 19.99 元在 JSON 里都是 number。把语义决策留给使用者,是这类前向生成器的合理边界,代价就是第二节那张替换表要自己过一遍。

Lombok 的 @Data 和手写 getter/setter 选哪个?

团队已用 Lombok 选 @Data,样板代码最少;不想引入编译期插件或与其他注解处理器冲突时选手写。两者对 Jackson 没有区别,它只认 getter/setter 或字段可见性。

嵌套对象生成的是 static 内部类,有影响吗?

对 Jackson 没有影响,static 内部类可以独立实例化。但工程上更常见的是把它拆成独立的顶层类放进包里,便于复用与单独测试——生成器的内部类结构适合快速查看,不适合长期维护。

List 字段反序列化报 ClassCastException 怎么定位?

先查是不是裸 List.class 反序列化:元素实际是 LinkedHashMap,取字段时才炸。改用 TypeReference 后,如果还报错,用本站报错信息反查页把异常信息贴进去按语言查对应条目。

参考资料

相关文章

JSON 转 TypeScript:接口、类型守卫与生成边界

从样例 JSON 生成 interface 只完成了一半:可选字段、null、联合类型、日期与大整数都要人工确认,生成结果本身也不做运行时校验。这篇给出生成、收敛与守卫的完整流程。

进阶约 9 分钟更新于 2026-11-04

大整数为什么变成 9007199254740992

超过 2^53-1 的整数在 IEEE 754 双精度里无法精确表示,JSON.parse 会静默把末位改掉。订单号、雪花 ID、金额分单位都踩过这个坑,正确解法是全程当字符串传。

进阶约 4 分钟更新于 2026-11-04

JSON 报错信息反查表:从报错原文直接定位改法

按 JSON.parse 报错原文反查原因与改法:12 条高频报错对应 Chrome、Firefox、Safari 三种措辞,覆盖 HTML 错误页、截断输入、重复解析、NDJSON 拼接等线上场景,并解释报错位置为什么常指向"下游"。

实战约 8 分钟更新于 2026-11-04