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.

Cue Club 2 on Apple Silicon

It's been quite a long time for me not to play a decent snooker game since I replaced my Windows laptop with an Apple Silicon one. To be...