diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java index b85f392a..4ebcd490 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java @@ -45,12 +45,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.*; @@ -69,6 +71,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; /** @@ -91,6 +94,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()); } /** @@ -105,6 +110,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 { + + 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 { + + 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 { + + 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. * diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/Jackson2HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/Jackson2HalIntegrationTest.java index b54f09dc..401f962f 100755 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/Jackson2HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/Jackson2HalIntegrationTest.java @@ -22,6 +22,7 @@ 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; @@ -49,6 +50,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.SerializationFeature; +import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; /** @@ -552,6 +554,21 @@ class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingIntegrationT .forEach(it -> assertThat(it).containsKey("someSample")); } + @Test // #1157 + void rendersMapContentCorrectly() throws Exception { + + Map 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"); + } + @Relation(collectionRelation = "someSample") static class SomeSample { String name;