I faced the same problem and tried to solve it with several techniques.
Implemented working solution - it's a dirty workaround so don't blame me for the quality of the code, probably I will clean it up later :)
I wanted to test Spring Data REST API and realized that MappingJackson2HttpMessageConverter ignores @Entity relations.
Setting serializer modifier didn't work correctly: null-value serializer didn't work and relations serialized with deep property serialization.
The idea of workaround is to provide CustomSerializerModifier which returns CustomSerializer for project @Entities (inherited from BaseEntity in this example). CustomSerializer performs following actions:
- write null-values (because omits them)
- provide array of related @Entities as List in Spring Data REST style (//)
- execute serialize(...) of default MappingJackson2HttpMessageConverter but providing NameTransformer which renames relations key (add ' _@ ') and then applies filter which excludes all fields starting with ' _@ '
I don't like this monster, but it works, and sadly I didn't find any solution :/
Working solution:
BasicRestTest
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.meddis.util.serializer.CustomIgnorePropertyFilter;
import com.meddis.util.serializer.CustomSerializerModifier;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.http.MockHttpOutputMessage;
import org.springframework.security.authentication.encoding.Md5PasswordEncoder;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.WebApplicationContext;
import java.io.IOException;
import java.nio.charset.Charset;
import static org.junit.Assert.assertNotNull;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
@RunWith(SpringRunner.class)
@ActiveProfiles({"test"})
@TestPropertySource(properties = {
"timezone = UTC"
})
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public abstract class BasicRestTest {
protected String host = "localhost";
@Value("${local.server.port}")
protected int port;
@Value("${spring.data.rest.basePath}")
protected String springDataRestBasePath;
protected MediaType contentType = new MediaType("application", "hal+json", Charset.forName("utf8"));
protected MockMvc mockMvc;
private static HttpMessageConverter mappingJackson2HttpMessageConverter;
protected ObjectMapper objectMapper;
@Autowired
private WebApplicationContext webApplicationContext;
@Autowired
void setConverters(HttpMessageConverter<?>[] converters) {
this.objectMapper = new ObjectMapper();
if (this.mappingJackson2HttpMessageConverter == null) {
this.mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
SimpleModule simpleModule = new SimpleModule("CUSTOM", Version.unknownVersion());
simpleModule.setSerializerModifier(new CustomSerializerModifier(springDataRestBasePath));
((MappingJackson2HttpMessageConverter) this.mappingJackson2HttpMessageConverter).getObjectMapper()
.registerModule(simpleModule);
FilterProvider fp = new SimpleFilterProvider().addFilter("CUSTOM", new CustomIgnorePropertyFilter());
((MappingJackson2HttpMessageConverter) this.mappingJackson2HttpMessageConverter).getObjectMapper()
.setFilterProvider(fp);
((MappingJackson2HttpMessageConverter) this.mappingJackson2HttpMessageConverter).setPrettyPrint(true);
}
assertNotNull("the JSON message converter must not be null", this.mappingJackson2HttpMessageConverter);
}
@Before
public void setup() throws Exception {
this.mockMvc = webAppContextSetup(webApplicationContext).build();
}
protected String json(final Object o) throws IOException {
MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage();
this.mappingJackson2HttpMessageConverter.write(o, MediaTypes.HAL_JSON, mockHttpOutputMessage);
return mockHttpOutputMessage.getBodyAsString();
}
}
CustomSerializerModifier
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.meddis.model.BaseEntity;
public class CustomSerializerModifier extends BeanSerializerModifier {
private final String springDataRestBasePath;
public CustomSerializerModifier(final String springDataRestBasePath) {
this.springDataRestBasePath = springDataRestBasePath;
}
@Override
public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
if (BaseEntity.class.isAssignableFrom(beanDesc.getBeanClass())) {
return new CustomSerializer((JsonSerializer<Object>) serializer, springDataRestBasePath);
}
return serializer;
}
}
CustomSerializer
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.util.NameTransformer;
import com.google.common.base.Preconditions;
import com.meddis.model.BaseEntity;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
public class CustomSerializer extends JsonSerializer<Object> {
private final JsonSerializer<Object> defaultSerializer;
private final String springDataRestBasePath;
public CustomSerializer(JsonSerializer<Object> defaultSerializer, final String springDataRestBasePath) {
this.defaultSerializer = Preconditions.checkNotNull(defaultSerializer);
this.springDataRestBasePath = springDataRestBasePath;
}
@SuppressWarnings("unchecked")
@Override
public void serialize(Object baseEntity, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException, JsonProcessingException {
jsonGenerator.writeStartObject();
Set<String> nestedEntityKeys = new HashSet<>();
Arrays.asList(baseEntity.getClass().getMethods()).stream()
.filter(field -> field.getName().startsWith("get"))
.filter(field -> !Arrays.asList("getClass", "getVersion").contains(field.getName()))
.forEach(field -> {
try {
Object value = field.invoke(baseEntity, new Object[]{});
String fieldName = field.getName().replaceAll("^get", "");
fieldName = fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1);
if (value == null) {
jsonGenerator.writeObjectField(fieldName, null);
} else if (Iterable.class.isAssignableFrom(value.getClass())) {
Iterator it = ((Iterable) value).iterator();
// System.out.println(field.getName() + field.invoke(baseEntity, new Object[]{}));
List<String> nestedUris = new ArrayList<>();
it.forEachRemaining(nestedValue -> {
if (BaseEntity.class.isAssignableFrom(nestedValue.getClass())) {
try {
String nestedEntityStringDataName = nestedValue.getClass().getSimpleName() + "s";
nestedEntityStringDataName = nestedEntityStringDataName.substring(0, 1).toLowerCase() + nestedEntityStringDataName.substring(1);
Long nestedId = (long) nestedValue.getClass().getMethod("getId").invoke(nestedValue, new Object[]{});
String nestedEntitySpringDataPath = springDataRestBasePath + "/" + nestedEntityStringDataName + "/" + Long.toString(nestedId);
nestedUris.add(nestedEntitySpringDataPath);
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ignored) {
}
}
});
nestedEntityKeys.add(fieldName);
jsonGenerator.writeObjectField(fieldName, nestedUris);
}
} catch (Throwable ignored) {
}
});
// Apply default serializer
((JsonSerializer<Object>) defaultSerializer.unwrappingSerializer(new NameTransformer() {
@Override
public String transform(String s) {
if (nestedEntityKeys.contains(s)) {
return "_@" + s;
}
return s;
}
@Override
public String reverse(String s) {
if (nestedEntityKeys.contains(s.substring(2))) {
return s.substring(2);
}
return s;
}
}).withFilterId("CUSTOM")).serialize(baseEntity, jsonGenerator, serializerProvider);
jsonGenerator.writeEndObject();
}
}
CustomIgnorePropertyFilter
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
public class CustomIgnorePropertyFilter extends SimpleBeanPropertyFilter {
@Override
public void serializeAsField(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider, PropertyWriter propertyWriter) throws Exception {
if (propertyWriter.getName().startsWith("_@")) {
return;
}
super.serializeAsField(o, jsonGenerator, serializerProvider, propertyWriter);
}
@Override
public void serializeAsElement(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider, PropertyWriter propertyWriter) throws Exception {
if (propertyWriter.getName().startsWith("_@")) {
return;
}
super.serializeAsElement(o, jsonGenerator, serializerProvider, propertyWriter);
}
@Override
public void depositSchemaProperty(PropertyWriter propertyWriter, ObjectNode objectNode, SerializerProvider serializerProvider) throws JsonMappingException {
if (propertyWriter.getName().startsWith("_@")) {
return;
}
super.depositSchemaProperty(propertyWriter, objectNode, serializerProvider);
}
@Override
public void depositSchemaProperty(PropertyWriter propertyWriter, JsonObjectFormatVisitor jsonObjectFormatVisitor, SerializerProvider serializerProvider) throws JsonMappingException {
if (propertyWriter.getName().startsWith("_@")) {
return;
}
super.depositSchemaProperty(propertyWriter, jsonObjectFormatVisitor, serializerProvider);
}
}
VideoStreamRestTest
import com.meddis.AdminApiTest;
import com.meddis.model.VideoStream;
import com.meddis.repository.SpecialistRepository;
import com.meddis.repository.VideoStreamTagRepository;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MvcResult;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
/**
* <a href="https://spring.io/guides/tutorials/bookmarks/">example</a>
*/
public class VideoStreamRestTest extends AdminApiTest {
@Autowired
private SpecialistRepository specialistRepository;
@Autowired
private VideoStreamTagRepository videoStreamTagRepository;
@Test
public void springDataRestVideoStreams() throws Exception {
String requestBody;
String newEntityTitle = md5("VIDEO_STREAM_");
MvcResult create = mockMvc.perform(post(springDataRestBasePath + "/videoStreams").headers(authenticationHeader)
.content(requestBody = json(new VideoStream()
.setTitle(newEntityTitle)
.setType(VideoStream.Type.BROADCAST)
.setPrice(10.0)
.setDurationInMinutes(70)
.setDescription("broadcast description")
.setPreviewUrl("http://example.com")
.setSpecialists(StreamSupport.stream(specialistRepository.findAll().spliterator(), false).collect(Collectors.toList()))
.setTags(StreamSupport.stream(videoStreamTagRepository.findAll().spliterator(), false).collect(Collectors.toList())))))
.andExpect(status().isCreated())
.andReturn();
String createdLocation = create.getResponse().getHeader("Location");
logger.info("Created new entity: {}", createdLocation);
logger.info("Sent: {}", requestBody);
MvcResult list = mockMvc.perform(get(springDataRestBasePath + "/videoStreams").headers(authenticationHeader))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$._embedded.videoStreams", hasSize(greaterThanOrEqualTo(1))))
.andExpect(jsonPath("$._embedded.videoStreams[*].title", hasItem(newEntityTitle)))
.andExpect(jsonPath("$._embedded.videoStreams[*]._links.self.href", hasItem(createdLocation)))
.andReturn();
logger.info("Got list containing new entity:\n{}", list.getResponse().getContentAsString());
MvcResult createdEntity = mockMvc.perform(get(createdLocation).headers(authenticationHeader))
.andExpect(status().isOk())
.andExpect(jsonPath("$._links.self.href", equalTo(createdLocation)))
.andExpect(jsonPath("$.title", equalTo(newEntityTitle)))
.andReturn();
logger.info("Got new entity:\n{}", createdEntity.getResponse().getContentAsString());
}
}
AdminApiTest
import com.fasterxml.jackson.databind.JsonNode;
import org.junit.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.web.servlet.MvcResult;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public abstract class AdminApiTest extends BasicRestTest {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
protected HttpHeaders authenticationHeader;
@Before
@Override
public void setup() throws Exception {
super.setup();
this.authenticationHeader = createHeaderWithAuthentication();
}
protected HttpHeaders createHeaderWithAuthentication() throws IOException {
String user = "pasha@pasha.ru";
String password = "pasha";
ResponseEntity<String> response = new TestRestTemplate()
.postForEntity(
"http://" + host + ":" + port
+ "login?"
+ "&username=" + user
+ "&password=" + password,
null,
String.class
);
assertEquals(HttpStatus.FOUND, response.getStatusCode());
List<String> authenticationCookie = response.getHeaders().get("Set-Cookie");
assertEquals(1, authenticationCookie.size());
HttpHeaders headers = new HttpHeaders();
headers.set("Cookie", authenticationCookie.get(0));
return headers;
}
}