Spring Boot + MyBatis 处理 JSON 字段的完整指南
前言最近在开发一个活动发布功能时遇到了一个让人头疼的问题前端传递的联系方式信息contactInfo是一个 JSON 对象但后端始终无法正确接收和存储到数据库。经过一番折腾终于完美解决了。本文将详细记录这个问题和解决方案希望对遇到类似问题的朋友有所帮助。问题场景我需要实现一个活动发布接口其中联系方式字段contactInfo需要存储多个信息如微信、QQ、手机号等设计如下数据库表结构CREATE TABLE activity ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, title VARCHAR(50), category VARCHAR(20), activity_type VARCHAR(50), activity_time DATETIME, city VARCHAR(50), people_limit INT, current_people INT, contact_info JSON, -- JSON 类型字段 contact_visibility VARCHAR(20), create_time DATETIME, status TINYINT );前端传参格式{ title: 今晚王者三排, category: GAME, activityType: 王者荣耀, activityTime: 2026-03-23 20:00:00, city: 南京, peopleLimit: 3, contactInfo: { wechat: test123, qq: 123456789, phone: 13800138000 }, contactVisibility: PUBLIC }遇到的坑坑1LocalDateTime 反序列化失败错误信息Cannot deserialize value of type java.time.LocalDateTime from String 2026-03-23 20:00:00原因Jackson 默认的日期格式是yyyy-MM-ddTHH:mm:ss但前端传的是yyyy-MM-dd HH:mm:ss空格分隔。解决方案在 DTO 中添加JsonFormat注解JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime activityTime;坑2contactInfo 类型不匹配错误信息Cannot deserialize value of type java.lang.String from Object value原因前端传的是对象{wechat:test123}但后端 DTO 定义的是String类型。坑3MyBatis 无法处理 JSON 字段错误信息Type handler was null on parameter mapping for property contactInfo原因MyBatis 默认不知道如何将 Java 对象转换为数据库的 JSON 字段。坑4自增 ID 返回 null错误信息Column activity_id cannot be null原因MyBatis insert 后没有返回自增 ID导致插入 activity_member 时 activity_id 为 null。解决方案经过多次尝试我选择了最简单直接的方案手动转换 JSON1. DTO 使用 Object 类型接收package com.jade.partnermatch.dto; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import java.time.LocalDateTime; Data public class ActivityCreateDTO { private String title; private String category; private String activityType; JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime activityTime; private String city; private Integer peopleLimit; private Object contactInfo; // 使用 Object 接收可以是字符串或对象 private String contactVisibility; }2. Entity 使用 String 存储package com.jade.partnermatch.entity; import lombok.Data; import java.time.LocalDateTime; Data public class Activity { private Long id; private Long userId; private String title; private String category; private String activityType; private LocalDateTime activityTime; private String city; private Integer peopleLimit; private Integer currentPeople; private String contactInfo; // 存 JSON 字符串 private String contactVisibility; private LocalDateTime createTime; private Integer status; }3. Service 手动转换package com.jade.partnermatch.service; import com.fasterxml.jackson.databind.ObjectMapper; import com.jade.partnermatch.dto.ActivityCreateDTO; import com.jade.partnermatch.entity.Activity; import com.jade.partnermatch.mapper.ActivityMapper; import com.jade.partnermatch.mapper.ActivityMemberMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; Service public class ActivityService { Autowired private ActivityMapper activityMapper; Autowired private ActivityMemberMapper activityMemberMapper; Autowired private ObjectMapper objectMapper; // 注入 Jackson Transactional public void create(ActivityCreateDTO dto) { Activity activity new Activity(); activity.setUserId(1L); activity.setTitle(dto.getTitle()); activity.setCategory(dto.getCategory()); activity.setActivityType(dto.getActivityType()); activity.setActivityTime(dto.getActivityTime()); activity.setCity(dto.getCity()); activity.setPeopleLimit(dto.getPeopleLimit()); activity.setCurrentPeople(1); // 关键将 Object 转换为 JSON 字符串 String contactInfoJson convertToJson(dto.getContactInfo()); activity.setContactInfo(contactInfoJson); activity.setContactVisibility(dto.getContactVisibility()); activity.setCreateTime(LocalDateTime.now()); activity.setStatus(0); // 先插入活动获取自增 ID activityMapper.insert(activity); // 再插入活动成员 activityMemberMapper.insert(activity.getId(), activity.getUserId()); } private String convertToJson(Object obj) { if (obj null) { return null; } try { return objectMapper.writeValueAsString(obj); } catch (Exception e) { e.printStackTrace(); return {}; } } }4. Mapper XML 配置自增 ID?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.jade.partnermatch.mapper.ActivityMapper !-- 关键添加 useGeneratedKeys 和 keyProperty -- insert idinsert parameterTypeActivity useGeneratedKeystrue keyPropertyid INSERT INTO activity (user_id, title, category, activity_type, activity_time, city, people_limit, current_people, contact_info, contact_visibility, create_time, status) VALUES (#{userId}, #{title}, #{category}, #{activityType}, #{activityTime}, #{city}, #{peopleLimit}, #{currentPeople}, #{contactInfo}, #{contactVisibility}, #{createTime}, #{status}) /insert select idlist resultTypeActivity SELECT * FROM activity WHERE status 0 ORDER BY create_time DESC /select /mapper5. 如果需要查询时解析 JSON// 在查询后解析 JSON 字符串为 Map public ListActivity list() { ListActivity activities activityMapper.list(); for (Activity activity : activities) { if (activity.getContactInfo() ! null) { try { MapString, Object contactInfoMap objectMapper.readValue( activity.getContactInfo(), Map.class ); // 使用 contactInfoMap... } catch (Exception e) { e.printStackTrace(); } } } return activities; }最终效果数据库成功存储 JSON 数据mysql select * from activity; ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | id | user_id | title | category | activity_type | activity_time | city | people_limit | current_people | contact_info | contact_visibility | create_time | status | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 1 | 1 | 今晚王者三排 | GAME | 王者荣耀 | 2026-03-23 20:00:00 | 南京 | 3 | 2 | {wechat: test123} | PUBLIC | 2026-03-24 20:22:59 | 0 | | 2 | 1 | 今晚王者三排 | GAME | 王者荣耀 | 2026-03-23 20:00:00 | 南京 | 3 | 1 | {wechat: test123} | PUBLIC | 2026-03-26 13:50:05 | 0 | | 3 | 1 | 今晚王者三排 | GAME | 王者荣耀 | 2026-03-23 20:00:00 | 南京 | 3 | 1 | {wechat: test123} | PUBLIC | 2026-03-26 13:51:45 | 0 | | 4 | 1 | 今晚王者三排 | GAME | 王者荣耀 | 2026-03-23 20:00:00 | 南京 | 3 | 1 | {wechat: test123} | PUBLIC | 2026-03-26 13:53:44 | 0 | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 4 rows in set (0.00 sec)方案优缺点优点简单直接不需要学习复杂的 TypeHandler完全可控自己决定如何转换易于调试兼容性好前端传字符串或对象都能处理代码清晰逻辑一目了然缺点查询时需要手动解析 JSON如果需要频繁操作 JSON 内的字段性能稍差其他可选方案方案2使用 MyBatis 的 JacksonTypeHandler在 Mapper XML 中配置result columncontact_info propertycontactInfo typeHandlerorg.apache.ibatis.type.JacksonTypeHandler/方案3自定义 TypeHandler实现BaseTypeHandler接口自己处理 JSON 转换。总结JSON 字段接收DTO 使用Object或Map类型JSON 字段存储Entity 使用String类型转换工具使用 Jackson 的ObjectMapper自增 IDMyBatis insert 添加useGeneratedKeys和keyProperty遇到类似问题不要慌一步步排查总能找到解决方案。希望这篇文章对你有帮助