highlight.js

Tuesday, July 5, 2022

Serialize and Deserialize PageImpl in Jackson

It's very common to put paginated query results in Redis via Jackson serialization and deserialization. However, org.springframework.data.domain.PageImpl doesn't expose a default constructor. Even worse, that applies to org.springframework.data.domain.Sort and org.springframework.data.domain.Sort.Order as well. There are a couple of workarounds. I'd like to introduce a less intrusive approach for your reference.

The idea is to write a Jackson module which injects a custom serializer and deserializer for org.springframework.data.domain.PageImpl. Let's see how to do that.

1. Define an interface.

public interface IJPADataPage {
String _CLASS = "@class";
String CONTENT = "content";
String DIRECTION = "direction";
String IGNORE_CASE = "ignoreCase";
String NULL_HANDLING = "nullHandling";
String NUMBER = "number";
String PROPERTY = "property";
String SIZE = "size";
String SORT = "sort";
String TOTAL_ELEMENTS = "totalElements";
}

2. Define a Serializer.

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Sort;

import java.io.IOException;

@SuppressWarnings("unchecked")
public class JPADataPageSerializer extends StdSerializer<PageImpl> implements IJPADataPage {
public JPADataPageSerializer(Class<PageImpl> t) {
super(t);
}

public JPADataPageSerializer() {
this(null);
}

@Override
public void serialize(
PageImpl value,
JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField(_CLASS, PageImpl.class.getName());
jsonGenerator.writePOJOField(CONTENT, value.getContent());
jsonGenerator.writeNumberField(NUMBER, value.getNumber());
jsonGenerator.writeNumberField(SIZE, value.getSize());
jsonGenerator.writeNumberField(TOTAL_ELEMENTS, value.getTotalElements());
jsonGenerator.writeArrayFieldStart(SORT);
if (!value.getSort().isEmpty()) {
for (Sort.Order order : value.getSort()) {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField(PROPERTY, order.getProperty());
jsonGenerator.writeStringField(DIRECTION, order.getDirection().name());
jsonGenerator.writeBooleanField(IGNORE_CASE, order.isIgnoreCase());
jsonGenerator.writeStringField(NULL_HANDLING, order.getNullHandling().name());
jsonGenerator.writeEndObject();
}
}
jsonGenerator.writeEndArray();
jsonGenerator.writeEndObject();
}

@Override
public void serializeWithType(
PageImpl value,
JsonGenerator jsonGenerator,
SerializerProvider serializerProvider,
TypeSerializer typeSer) throws IOException {
serialize(value, jsonGenerator, serializerProvider);
}
}

3. Define a Deserializer.

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.node.ArrayNode;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@SuppressWarnings("unchecked")
public class JPADataPageDeserializer extends StdDeserializer<PageImpl> implements IJPADataPage {
public JPADataPageDeserializer(Class<?> type) {
super(type);
}

public JPADataPageDeserializer() {
this(null);
}

@Override
public PageImpl deserialize(
JsonParser jsonParser,
DeserializationContext deserializationContext) throws IOException {
JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser);
List<?> content = deserializationContext.readTreeAsValue(jsonNode.get(CONTENT), List.class);
int number = jsonNode.get(NUMBER).asInt(0);
int size = jsonNode.get(SIZE).asInt(1);
int totalElements = jsonNode.get(TOTAL_ELEMENTS).asInt(0);
List<Sort.Order> orders = new ArrayList<>();
ArrayNode arrayNode = (ArrayNode) jsonNode.get(SORT);
if (!arrayNode.isEmpty()) {
for (JsonNode jsonNodeOrder : arrayNode) {
String property = jsonNodeOrder.get(PROPERTY).asText();
Sort.Direction direction = Sort.Direction.valueOf(jsonNodeOrder.get(DIRECTION).asText());
boolean ignoreCase = jsonNodeOrder.get(IGNORE_CASE).asBoolean();
Sort.NullHandling nullHandling = Sort.NullHandling.valueOf(jsonNodeOrder.get(NULL_HANDLING).asText());
Sort.Order order = new Sort.Order(direction, property, nullHandling);
if (ignoreCase) {
order = order.ignoreCase();
}
orders.add(order);
}
}
PageRequest pageRequest = PageRequest.of(number, size, Sort.by(orders));
return new PageImpl(content, pageRequest, totalElements);
}

@Override
public Object deserializeWithType(
JsonParser jsonParser,
DeserializationContext deserializationContext,
TypeDeserializer typeDeserializer) throws IOException {
return deserialize(jsonParser, deserializationContext);
}
}

4. Define a module.

import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.data.domain.PageImpl;

public class JPADataModule extends SimpleModule {
public static final String NAME = "JPA Data Module";
public static final Version VERSION = new Version(0, 1, 0, null, null, null);

public JPADataModule() {
super(NAME, VERSION);
addSerializer(PageImpl.class, new JPADataPageSerializer());
addDeserializer(PageImpl.class, new JPADataPageDeserializer());
}
}

5. Register the module.

objectMapper.registerModule(new JPADataModule());

Now Jackson is able to handle PageImpl and the Redis cache works.

Serialize and Deserialize PageImpl in Jackson

It's very common to put paginated query results in Redis via Jackson serialization and deserialization. However, org.springframework.dat...