Hibernate custom UserType to store field value as Json string

In one of my project, I wanted to store the value of a domain object field as json string in database. Below is an example how it can be done.

Hibernate json user type

import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.type.SimpleType
import groovy.transform.CompileStatic
import org.hibernate.engine.spi.SessionImplementor
import org.hibernate.usertype.ParameterizedType
import org.hibernate.usertype.UserType

import java.sql.PreparedStatement
import java.sql.ResultSet
import java.sql.SQLException
import java.sql.Types

@CompileStatic
class JsonUserType implements UserType, ParameterizedType {

	private static final int[] SQL_TYPES = [Types.LONGVARCHAR] as int[]
	private Class<?> returnedClass;

	@Override
	public boolean equals(Object x, Object y) throws HibernateException {
		if (x == y) {
			return true;
		} else if (x == null || y == null) {
			return false;
		} else {
			return x.equals(y);
		}
	}

	@Override
	public int hashCode(Object x) throws HibernateException {
		return null == x ? 0 : x.hashCode();
	}

	@Override
	public boolean isMutable() {
		return true;
	}

	@Override
	public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
		if(value == null) {
			st.setNull(index, Types.VARCHAR)
		} else {
			String s = convertObjectToJson(value)
			st.setString(index, s)
		}
	}

	@Override
	public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException {
		String value = rs.getString(names[0])
		def result = null
		if (value != null && !value.equals("")) {
			try {
				result = convertJsonToObject(value)
			} catch (IOException e) {
				throw new HibernateException("Exception deserializing value " + value, e);
			}
		}
		return result;
	}

	Object convertJsonToObject(String content) {
		if ((content == null) || (content.isEmpty())) {
			return null;
		}
		try {
			ObjectMapper mapper = new ObjectMapper()
			mapper.enableDefaultTyping()
			JavaType type = createJavaType(mapper)
			if (type == null)
				return mapper.readValue(content, returnedClass);

			return mapper.readValue(content, type);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	String convertObjectToJson(Object object) {
		try {
			ObjectMapper mapper = new ObjectMapper()
			mapper.enableDefaultTyping()
			mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
			return mapper.writeValueAsString(object);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public Object deepCopy(Object value) throws HibernateException {
		String json = convertObjectToJson(value);
		return convertJsonToObject(json);
	}

	@Override
	public Object replace(Object original, Object target, Object owner) throws HibernateException {
		return deepCopy(original);
	}

	
	@Override
	public Serializable disassemble(Object value) throws HibernateException {
		return (Serializable) deepCopy(value);
	}

	
	@Override
	public Object assemble(Serializable cached, Object owner) throws HibernateException {
		return deepCopy(cached);
	}

	
	public JavaType createJavaType(ObjectMapper mapper) {
		try {
			return SimpleType.construct(returnedClass());
		} catch (IllegalArgumentException e) {
			return null;
		}
	}

	@Override
	public int[] sqlTypes() {
		return SQL_TYPES;
	}

	@Override
	public void setParameterValues(Properties parameters) {
			this.returnedClass = Class.forName(parameters.getProperty('clazz'))

	}

	@Override
	public Class<?> returnedClass() {
		return this.returnedClass;
	}

}

Gorm mapping for the json user type

 Page {
       
       PageBody body 

	static mapping = {
		body type: JsonUserType, params: [clazz: PageBody.name]
	}

	static constraints = {
		body nullable: true
		
	}

}

Storing a list type field as json

class BlogPage extends Page {

	List<Region> regions

	static mapping = {
		regions type: JsonUserType, params: [clazz: ArrayList.name]
	}

	static constraints = {
		regions nullable: true
	}

}

 

This is a quick and dirty implementation. There's scope to improve it further. eg. You can modify it to take the advantage of database system's native json column type if your database supports it.