Mastering MyBatis JSON Field Mapping with Custom TypeHandlers
This article walks through three ways to map JSON columns to Java objects in MyBatis, explains why simple String or JSONObject approaches are fragile, and provides a complete custom TypeHandler implementation, registration, and usage example for complex entity fields.
Mapping JSON columns to Java fields
Three common approaches are presented:
String column – store JSON as plain text and parse manually on each access.
JSONObject libraries – use com.alibaba.fastjson.JSONObject or cn.hutool.json.JSONObject to parse the string, which still requires explicit key extraction and adds an extra dependency.
Custom MyBatis TypeHandler – define a handler that converts between a Java type and the JDBC VARCHAR column.
BaseTypeHandler abstraction
The MyBatis class org.apache.ibatis.type.BaseTypeHandler<T> provides a generic base for custom handlers. The essential methods are:
setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType)– writes the Java value to the SQL statement. getNullableResult(ResultSet rs, String columnName) and overloads – read the column value and convert it back to T.
Two abstract hooks that subclasses must implement: protected abstract String convert(T obj) – serialize the Java object to a string. protected abstract T parse(String result) – deserialize the string to the Java object.
public abstract class BaseTypeHandler<T> extends org.apache.ibatis.type.BaseTypeHandler<T> {
public Class<T> type() {
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), 0);
}
protected abstract String convert(T obj);
protected abstract T parse(String result);
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null) return;
ps.setString(i, this.convert(parameter));
}
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
String str = rs.getString(columnName);
return str == null || str.isEmpty() ? null : this.parse(str);
}
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String str = rs.getString(columnIndex);
return str == null || str.isEmpty() ? null : this.parse(str);
}
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String str = cs.getString(columnIndex);
return str == null || str.isEmpty() ? null : this.parse(str);
}
}JsonStringTypeHandler
A concrete abstract subclass handles JSON serialization using JsonKit. It supports both single POJOs and arrays of POJOs (array, not List).
public abstract class JsonStringTypeHandler<T> extends BaseTypeHandler<T> {
private Class<?> componentType;
private Object[] componentArray;
public JsonStringTypeHandler() {
Class<T> tClass = type();
if (tClass.isArray()) {
Class<Object[]> arrayClass = (Class<Object[]>) tClass;
this.componentType = arrayClass.getComponentType();
this.componentArray = (Object[]) Array.newInstance(componentType, 0);
}
log.info("Loading {}, type: {}", this.getClass().getSimpleName(), type().getSimpleName());
}
@Override
protected String convert(T obj) {
return JsonKit.toJson(obj);
}
@Override
protected T parse(String json) {
if (this.componentType != null) {
List<?> list = JsonKit.toList(json, this.componentType);
if (list == null) return null;
return (T) list.toArray(this.componentArray);
}
return JsonKit.toObject(json, type());
}
}Spring‑boot registration
All beans that extend BaseTypeHandler are injected and registered with MyBatis’ TypeHandlerRegistry during application startup.
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(DataSource.class)
@AllArgsConstructor
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisConfiguration.class})
public class MybatisPlusConfig implements InitializingBean {
final MybatisPlusProperties mybatisPlusProperties;
final List<BaseTypeHandler> jsonStringTypeHandlers;
@Override
public void afterPropertiesSet() {
MybatisConfiguration configuration = mybatisPlusProperties.getConfiguration();
if (configuration == null) {
configuration = new MybatisConfiguration();
mybatisPlusProperties.setConfiguration(configuration);
}
TypeHandlerRegistry registry = configuration.getTypeHandlerRegistry();
if (jsonStringTypeHandlers != null && !jsonStringTypeHandlers.isEmpty()) {
for (BaseTypeHandler handler : jsonStringTypeHandlers) {
registry.register(handler.type(), handler);
}
}
}
}Entity definition
After registration, JSON fields can be declared directly in entity classes without additional annotations.
@Data
@TableName("order_info")
public class OrderInfo {
private UserInfoVO userInfo; // JSON snapshot of the user
private GoodsSpuVO[] goodsSpus; // JSON array of product snapshots
}Two concrete handlers are created and annotated with @Component so that Spring discovers them:
@Component
public class UserInfoTypeHandler extends JsonStringTypeHandler<UserInfoVO> {}
@Component
public class GoodsSpusTypeHandler extends JsonStringTypeHandler<GoodsSpuVO[]> {}Limitation
The current implementation can map JSON arrays only to Java arrays; mapping to List<T> is not supported by MyBatis.
Additional handlers
Other custom storage formats (e.g., simple string lists, numeric arrays) are implemented as additional TypeHandler subclasses and integrated into the D3Boot framework. Source code is available at https://gitee.com/jensvn/d3boot.
Architect's Journey
E‑commerce, SaaS, AI architect; DDD enthusiast; SKILL enthusiast
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
