Thursday, May 10, 2018

Spring Boot 2 generic JPA converter to serialize and deserialize an entity attribute as JSON

As I wrote in my previous blog post, I needed to store some custom attributes of entities as JSON strings in the database.

Sometimes an entity attribute is not just a native or simple type like a Date, Long or String. A Java POJO like structure is required as an attribute. One might argue that a separate entity should be created which results in a separate table and a reference to it should be used like in a OneToOne or ManyToOne or OneToMany type of relationship. But there are special business conditions where creating a separate entity is not feasible. So, storing this entity as a JSON string is a good solution.

Purpose of this blog post

In the last blog post, I shared how I wrote a generic encryption decryption JPA Converter. In this blog post I will show you how to store a custom Java POJO like attribute as a JSON string in a database table column.

JPA allows writing 'conversion' code so that an entity attribute (table column) can be converted from one type to another and back. Though this is not a latest feature of JPA nor a bleeding edge topic, I am sharing how I wrote a generic converter for serialization and serialization of a Java POJO object to a string type table column.

Java and JSON

I am assuming you know what JSON is and how it is used for data transformation. If you need to know more then click here.

In our case, we need to convert some Java POJO like JPA attribute to its JSON equivalent and store it in the database table column. Similarly, we need to parse the JSON format and convert it back to the original Java POJO like attribute.

Java and Jackson

Given that JSON is not a complex format, one can attempt to convert a Java POJO like object into a JSON String and back by writing code for each such object. However, this is a tedious and error-prone endeavor. And why do it all yourself when there is a reliable, widely used library to do this? Faster Jackson is an awesome library to help convert your Java POJO like object into JSON (and XML too) and parse the JSON back into the original Java POJO object.

If you are new to Jackson or want to read more, then click here. There is a good tutorial here. For a more Spring framework specific tutorial, click here.

JPA and Converter

JPA allows you to apply a @Converter annotation to any Entity attribute i.e. to any table column definition in your table defintion. This is possible via the AttributeConveter interface. A class implementing this interface needs to implement the methods from this interface


  • Y convertToDatabaseColumn(X attribute) - Converts the value stored in the entity attribute into the data representation to be stored in the database.
  • X convertToEntityAttribute(Y dbData) -  Converts the data stored in the database column into the value to be stored in the entity attribute. Note that it is the responsibility of the converter writer to specify the correct dbData type for the corresponding column for use by the JDBC driver: i.e., persistence providers are not expected to do such type conversion.

Writing the generic converter

Please keep these two Jackson JSON javadocs open in your browser so that you can refer to them while reading the code I am presenting.
Though we do not need it for this converter, you can also look at the Jackson JSON Annotation javadocs for more details.

Note that for a good error free conversion of your Java POJO like object, one of the requirements is to provide the class type information to the Jackson ObjectMapper. It is not an absolute must, but having it is better. Also for a generic converter, it becomes quite essential to tell the converter about the Class type of the object.

The generic JSON JPA converter code

Here is the converter code. We just need one concrete class which can then be extended to convert a specific Java POJO like attribute.


package com.sunitkatkar.blogspot.converters;

import java.io.IOException;

import javax.persistence.AttributeConverter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;

/**
 * JPA converter class which converts any POJO like object to a JSON string and
 * stores it in the database table column. The JSON string contains the class
 * name of the POJO and its JSON format string. <br/>
 * 
 * Note: An abstract class is not needed as this class takes care of all the
 * JSON serialization and deserialization. You only need to extend this class so
 * that you can use your POJO class for conversion. The extended class should be
 * annotated with the {@literal @}Converter annotation
 * 
 * @author Sunit Katkar, sunitkatkar@gmail.com
 *         (https://sunitkatkar.blogspot.com/)
 * @since ver 1.0 (May 2018)
 * @version 1.0
 * @param <X>
 */
public class GenericJsonAttributeConverter<X>
        implements AttributeConverter<X, String> {

    protected static final Logger LOG = LoggerFactory
            .getLogger(GenericJsonAttributeConverter.class);

    /**
     * To 'write' the attribute POJO as a JSON string
     */
    private final ObjectWriter writer;

    /**
     * To 'read' the JSON string and convert to the attribute POJO
     */
    private final ObjectReader reader;

    /**
     * Default constructor
     */
    public GenericJsonAttributeConverter() {

        ObjectMapper mapper = new ObjectMapper();

        // We want all keys in the key value pair JSON even if the key has no
        // value
        mapper.setSerializationInclusion(Include.ALWAYS);

        //@formatter:off
        // The mapper should be able to 'see' the entity attributes (fields)
        mapper.setVisibility(mapper.getSerializationConfig()
                .getDefaultVisibilityChecker()
                .withFieldVisibility(JsonAutoDetect.Visibility.ANY)
                .withGetterVisibility(JsonAutoDetect.Visibility.NONE)
                .withIsGetterVisibility(JsonAutoDetect.Visibility.NONE));
        //@@formatter:on

        // We are wrapping the entity attribute in a class. See class for
        // details.
        reader = mapper.reader().forType(JsonTypeLike.class);
        writer = mapper.writer().forType(JsonTypeLike.class);
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * javax.persistence.AttributeConverter#convertToDatabaseColumn(java.lang.
     * Object)
     */
    @Override
    public String convertToDatabaseColumn(X attribute) {
        try {
            if (attribute != null) {
                JsonTypeLike<X> wrapper = new JsonTypeLike<X>(attribute,
                        writer);
                String value = writer.writeValueAsString(wrapper);
                return value;
            } else {
                return null;
            }
        } catch (JsonProcessingException e) {
            LOG.error("Failed to serialize as object into JSON: {}", attribute,
                    e);
            throw new RuntimeException(e);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * javax.persistence.AttributeConverter#convertToEntityAttribute(java.lang.
     * Object)
     */
    @Override
    public X convertToEntityAttribute(String dbData) {
        try {
            if (dbData != null) {
                JsonTypeLike<X> wrapper = reader.readValue(dbData);
                X obj = wrapper.readValue(reader);
                return obj;
            } else {
                return null;
            }
        } catch (IOException e) {
            LOG.error("Failed to deserialize as object from JSON: {}", dbData,
                    e);
            throw new RuntimeException(e);
        }
    }

    /**
     * The concrete type is needed for Jackson to serialize or deserialize. This
     * class is created to wrap the entity type &lt;Y&gt; so that the Jackson
     * {@link ObjectReader#forType(Class)} and
     * {@link ObjectWriter#forType(Class))} can be used to get the concrete type
     * of the attribute being serialized/deserialized.
     * 
     * @author Sunit Katkar, sunitkatkar@gmail.com
     *
     * @param <Y>
     */
    public static class JsonTypeLike<Y> {

        // For adding the type (class) of the entity in the generated JSON
        private String entityType;

        // For the actual value which is obtained by reading or writing the JSON
        private String entityValue;

        public JsonTypeLike() {
        }

        /**
         * Constructor which helps initialize the ObjectWriter by providing the
         * concrete class type to the writer
         * 
         * @param obj
         * @param writer
         */
        public JsonTypeLike(Y obj, ObjectWriter writer) {
            // TODO: Need a better way to do type conversion and not get safety
            // warning
            Class<Y> classType = ((Class<Y>) obj.getClass());
            // We are saving the class type as a string in entityType so that
            // while reading
            // back the JSON, the target Class type is known o the mapper.
            this.entityType = obj.getClass().getName();

            try {
                this.entityValue = writer.forType(classType)
                        .writeValueAsString(obj);
            } catch (JsonProcessingException e) {
                LOG.error("Failed serializing object to JSON: {}", obj, e);
            }
        }

        /**
         * Read the JSON format string and create the target Java POJO object
         * 
         * @param reader
         * @return
         */
        public Y readValue(ObjectReader reader) {
            try {
                // Once the json string is read, we need to know what is the
                // destination class
                // type. So reading the entityType value helps in getting the
                // Class details.
                Class<?> clazz = Class.forName(this.entityType);
                Y obj = reader.forType(clazz).readValue(this.entityValue);
                return obj;
            } catch (ClassNotFoundException | IOException e) {
                LOG.error("Failed deserializing object from JSON: {}",
                        this.entityValue, e);
                return null;
            }
        }

        public String getEntityType() {
            return entityType;
        }

        public void setEntityType(String type) {
            this.entityType = type;
        }

        public String getEntityValue() {
            return entityValue;
        }

        public void setValue(String value) {
            this.entityValue = value;
        }
    }
}

Example for using the converter

Let us suppose there is a class called Student and it contains an attribute of another custom type called Course. The Course is a simple Java POJO which contains details about the course that a student enrolls in.

JPA Entity - Student

This JPA entity has a Course attribute. The Student entity will create a table called 'student' in the database and have a column/field called 'course'. This course attribute is the one which needs to be stored as a JSON string.

package com.sunitkatkar.blogspot.model;

import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.sunitkatkar.blogspot.converters.CourseJsonConverter;

/**
 * JPA Entity which will be saved in the database table named 'student'. The
 * attribute (or table column) <tt>course</tt> needs to be converted to a JSON
 * string for storage and converted back to {@link Course} POJO when read.
 * 
 * @author Sunit Katkar, sunitkatkar@gmail.com
 *         (https://sunitkatkar.blogspot.com/)
 * @since ver 1.0 (May 2018)
 * @version 1.0
 */
@Entity
@Table(name = "student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "student_id")
    private Long id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    /**
     * This attribute is the Course POJO which needs to be stored as a JSON
     * string
     */
    @Convert(converter = CourseJsonConverter.class)
    @Column(name = "course")
    private Course course;

    /**
     * Just a field to get the Course POJO as a JSON string and display on the
     * UI
     */
    private String json;

    /**
     * @return the json
     */
    /**
     * Default constructor
     */
    public Student() {
    }

    // Getters and setters
    /**
     * @return the id
     */
    public Long getId() {
        return id;
    }

    /**
     * @param id
     *            the id to set
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * @return the firstName
     */
    public String getFirstName() {
        return firstName;
    }

    /**
     * @param firstName
     *            the firstName to set
     */
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    /**
     * @return the lastName
     */
    public String getLastName() {
        return lastName;
    }

    /**
     * @param lastName
     *            the lastName to set
     */
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    /**
     * @return the course
     */
    public Course getCourse() {
        return course;
    }

    /**
     * @param course
     *            the course to set
     */
    public void setCourse(Course course) {
        this.course = course;
    }

    /**
     * Method to get the Course POJO as a JSON string for display on UI
     * 
     * @return
     */
    public String getJson() {
        ObjectMapper mapper = new ObjectMapper();
        ObjectWriter writer = mapper.writer().forType(Course.class);

        String jsonString = "";
        try {
            jsonString = writer.withDefaultPrettyPrinter()
                    .writeValueAsString(this.course);
            // System.out.println(jsonString);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return jsonString;
    }

    /**
     * @param json
     *            the json to set
     */
    public void setJson(String json) {
        this.json = json;
    }
}

Course Java POJO

This is the Course attribute in the Student JPA entity. Do note that when you use java.util.Date fields you must annotate them with a @DateTimeFormat annotation so that Spring understands how to convert a string in a date like format to an actual date format. This is especially applicable when you have a browser based UI to enter dates.

package com.sunitkatkar.blogspot.model;

import java.util.Date;

import org.springframework.format.annotation.DateTimeFormat;

/**
 * This POJO is a attribute of the {@link Student} entity. This will be
 * serialized as a JSON string and stored in the 'course' field in the 'student'
 * table.
 * 
 * @author Sunit Katkar, sunitkatkar@gmail.com
 *         (https://sunitkatkar.blogspot.com/)
 * @since ver 1.0 (May 2018)
 * @version 1.0
 */
public class Course {

    private Long id;

    private String title;

    private String description;

    /**
     * The @DateTimeFormat annotation is required as the browser sends a date in
     * string format and Spring needs to translate it into a date. So we have to
     * specify a format for the date. Note that the browser always sends date in
     * the 'yyyy-MM-dd' format when using the HTML5 attribute type='date' on an
     * 'input' tag.
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date startDate;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date endDate;

    /**
     * 
     */
    public Course() {
        super();
    }

    /**
     * @return the id
     */
    public Long getId() {
        return id;
    }

    /**
     * @param id
     *            the id to set
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * @return the title
     */
    public String getTitle() {
        return title;
    }

    /**
     * @param title
     *            the title to set
     */
    public void setTitle(String title) {
        this.title = title;
    }

    /**
     * @return the description
     */
    public String getDescription() {
        return description;
    }

    /**
     * @param description
     *            the description to set
     */
    public void setDescription(String description) {
        this.description = description;
    }

    /**
     * @return the startDate
     */
    public Date getStartDate() {
        return startDate;
    }

    /**
     * @param startDate
     *            the startDate to set
     */
    public void setStartDate(Date startDate) {
        this.startDate = startDate;
    }

    /**
     * @return the endDate
     */
    public Date getEndDate() {
        return endDate;
    }

    /**
     * @param endDate
     *            the endDate to set
     */
    public void setEndDate(Date endDate) {
        this.endDate = endDate;
    }

}

Using the JSON JPA Converter

To use the converter, all you need to do is create a class which extends from it. The following example is applicable for the Student entity and its attribute Course. That's all there is to it.

import javax.persistence.Converter;

import com.sunitkatkar.blogspot.model.Course;

/**
 * Concrete converter class which extends the {@link GenericJsonAttributeConverter}
 * class. <br/>
 * Note: No need to do anything here. This is used to pass the attribute type to
 * the GenericJsonAttributeConverter
 * 
 * @author Sunit Katkar, sunitkatkar@gmail.com
 *         (https://sunitkatkar.blogspot.com/)
 * @since ver 1.0 (May 2018)
 * @version 1.0
 */
@Converter(autoApply = false)
public class CourseJsonConverter extends GenericJsonAttributeConverter<Course> {

}

Resources

I have included a complete working Spring Boot application to demonstrate how this JSON converter can be used. Visit my Github repository to download the code. You are free to fork it, but just request you to send me an email when you do so.

Happy coding :)

No comments: