#1158 - Register custom serializers to handle Map based EntityModel instances.
We now register a custom serializer to massage EntityModel instances into dedicated types that – in case of a Map being the content of the entity model – wrap the model into a type that applies the necessary Jackson tweaks to properly unwrap a Map.
This commit is contained in:
@@ -40,12 +40,14 @@ import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAnyGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParseException;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonStreamContext;
|
||||
import com.fasterxml.jackson.core.JsonToken;
|
||||
import com.fasterxml.jackson.core.Version;
|
||||
import com.fasterxml.jackson.databind.*;
|
||||
@@ -63,6 +65,7 @@ import com.fasterxml.jackson.databind.ser.ContainerSerializer;
|
||||
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
|
||||
import com.fasterxml.jackson.databind.ser.std.MapSerializer;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
|
||||
/**
|
||||
@@ -85,6 +88,8 @@ public class Jackson2HalModule extends SimpleModule {
|
||||
setMixInAnnotation(Link.class, LinkMixin.class);
|
||||
setMixInAnnotation(RepresentationModel.class, RepresentationModelMixin.class);
|
||||
setMixInAnnotation(CollectionModel.class, CollectionModelMixin.class);
|
||||
|
||||
addSerializer(EntityModel.class, new EntityModelSerializer());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,6 +104,75 @@ public class Jackson2HalModule extends SimpleModule {
|
||||
return LinkMixin.class.equals(mapper.findMixInClassFor(Link.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom serializer for {@link EntityModel} to make sure we get {@link Map} instances properly unwrapped. The
|
||||
* serializer wraps the model instance into a dedicated classes that apply the proper Jackson configuration that's
|
||||
* needed to achieve this and serializes those.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @see https://github.com/FasterXML/jackson-databind/issues/171
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
static class EntityModelSerializer extends StdSerializer<EntityModel> {
|
||||
|
||||
private static final long serialVersionUID = -5933309398043585183L;
|
||||
|
||||
public EntityModelSerializer() {
|
||||
super(EntityModel.class, false);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider)
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("null")
|
||||
public void serialize(EntityModel value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
|
||||
Object content = value.getContent();
|
||||
|
||||
RepresentationModel<?> wrapped = Map.class.isInstance(content) //
|
||||
? new MapModel(value) //
|
||||
: new NonMapModel(value);
|
||||
|
||||
provider.defaultSerializeValue(wrapped, gen);
|
||||
}
|
||||
|
||||
static class MapModel extends RepresentationModel<MapModel> {
|
||||
|
||||
private final @Nullable Map<?, ?> map;
|
||||
|
||||
public MapModel(EntityModel<?> model) {
|
||||
|
||||
super(model.getLinks().toList());
|
||||
|
||||
this.map = (Map<?, ?>) model.getContent();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@JsonAnyGetter
|
||||
public Map<?, ?> getContent() {
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
static class NonMapModel extends RepresentationModel<NonMapModel> {
|
||||
|
||||
private final EntityModel<?> model;
|
||||
|
||||
public NonMapModel(EntityModel<?> model) {
|
||||
super(model.getLinks().toList());
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@JsonUnwrapped
|
||||
public Object getContent() {
|
||||
return model.getContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link JsonSerializer} to render Link instances in HAL compatible JSON.
|
||||
*
|
||||
|
||||
@@ -22,8 +22,10 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -43,6 +45,8 @@ import org.springframework.hateoas.server.core.EmbeddedWrappers;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
|
||||
/**
|
||||
* Integration tests for Jackson 2 HAL integration.
|
||||
@@ -88,8 +92,8 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg
|
||||
DefaultLinkRelationProvider.INSTANCE);
|
||||
|
||||
mapper.registerModule(new Jackson2HalModule());
|
||||
mapper.setHandlerInstantiator(
|
||||
new HalHandlerInstantiator(provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY, new HalConfiguration()));
|
||||
mapper.setHandlerInstantiator(new HalHandlerInstantiator(provider, CurieProvider.NONE,
|
||||
MessageResolver.DEFAULTS_ONLY, new HalConfiguration()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -480,8 +484,9 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg
|
||||
|
||||
AnnotationLinkRelationProvider provider = new AnnotationLinkRelationProvider();
|
||||
|
||||
mapper.setHandlerInstantiator(new HalHandlerInstantiator(provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY,
|
||||
new HalConfiguration().withRenderSingleLinksFor("foo", RenderSingleLinks.AS_ARRAY)));
|
||||
mapper
|
||||
.setHandlerInstantiator(new HalHandlerInstantiator(provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY,
|
||||
new HalConfiguration().withRenderSingleLinksFor("foo", RenderSingleLinks.AS_ARRAY)));
|
||||
|
||||
RepresentationModel<?> resource = new RepresentationModel<>();
|
||||
resource.add(new Link("/some-href", "foo"));
|
||||
@@ -511,6 +516,21 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg
|
||||
}).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test // #1157
|
||||
void rendersMapContentCorrectly() throws Exception {
|
||||
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("key", "value");
|
||||
map.put("anotherKey", "anotherValue");
|
||||
|
||||
EntityModel<?> model = new EntityModel<>(map, new Link("foo", IanaLinkRelations.SELF));
|
||||
|
||||
DocumentContext context = JsonPath.parse(mapper.writeValueAsString(model));
|
||||
|
||||
assertThat(context.read("$.key", String.class)).isEqualTo("value");
|
||||
assertThat(context.read("$.anotherKey", String.class)).isEqualTo("anotherValue");
|
||||
}
|
||||
|
||||
private void verifyResolvedTitle(String resourceBundleKey) throws Exception {
|
||||
|
||||
LocaleContextHolder.setLocale(Locale.US);
|
||||
|
||||
Reference in New Issue
Block a user