diff --git a/src/main/java/org/springframework/hateoas/EntityModel.java b/src/main/java/org/springframework/hateoas/EntityModel.java index 24618caf..cce4a520 100644 --- a/src/main/java/org/springframework/hateoas/EntityModel.java +++ b/src/main/java/org/springframework/hateoas/EntityModel.java @@ -15,20 +15,24 @@ */ package org.springframework.hateoas; +import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.databind.util.NameTransformer; /** * A simple {@link EntityModel} wrapping a domain object and adding links to it. @@ -116,10 +120,11 @@ public class EntityModel extends RepresentationModel> { * * @return the content */ - @JsonUnwrapped @Nullable + @JsonUnwrapped + @JsonSerialize(using = MapSuppressingUnwrappingSerializer.class) public T getContent() { - return !Map.class.isInstance(content) ? content : null; + return content; } // Hacks to allow deserialization into an EntityModel> @@ -127,7 +132,7 @@ public class EntityModel extends RepresentationModel> { @Nullable @JsonAnyGetter @SuppressWarnings("unchecked") - public Map getMapContent() { + private Map getMapContent() { return Map.class.isInstance(content) ? (Map) content : null; } @@ -189,4 +194,36 @@ public class EntityModel extends RepresentationModel> { result += content == null ? 0 : 17 * content.hashCode(); return result; } + + private static class MapSuppressingUnwrappingSerializer extends StdSerializer { + + public MapSuppressingUnwrappingSerializer() { + super(Object.class); + } + + /* + * (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 + public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException { + + if (value == null || Map.class.isInstance(value)) { + return; + } + + provider.findValueSerializer(value.getClass()) // + .unwrappingSerializer(NameTransformer.NOP) // + .serialize(value, gen, provider); + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.JsonSerializer#isUnwrappingSerializer() + */ + @Override + public boolean isUnwrappingSerializer() { + return true; + } + } } 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 18c4668b..108914ed 100755 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/Jackson2HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/Jackson2HalIntegrationTest.java @@ -26,7 +26,6 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.TreeMap; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; @@ -60,8 +59,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.type.TypeFactory; +import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.Option; /** * Integration tests for Jackson 2 HAL integration. @@ -565,7 +566,7 @@ class Jackson2HalIntegrationTest { .forEach(it -> assertThat(it).containsKey("someSample")); } - @Test // #1157 + @Test // #1157, #1352 void rendersMapContentCorrectly() throws Exception { Map map = new HashMap<>(); @@ -574,10 +575,12 @@ class Jackson2HalIntegrationTest { EntityModel model = EntityModel.of(map, Link.of("foo", IanaLinkRelations.SELF)); - DocumentContext context = JsonPath.parse(mapper.writeValueAsString(model)); + DocumentContext context = JsonPath.parse(mapper.writeValueAsString(model), + Configuration.defaultConfiguration().addOptions(Option.SUPPRESS_EXCEPTIONS)); assertThat(context.read("$.key", String.class)).isEqualTo("value"); assertThat(context.read("$.anotherKey", String.class)).isEqualTo("anotherValue"); + assertThat(context.read("$.content", Object.class)).isNull(); } @Test // #1157 @@ -592,8 +595,7 @@ class Jackson2HalIntegrationTest { EntityModel> result = mapper.readValue(source, modelType); - assertThat(result.getContent()).isNull(); - assertThat(result.getMapContent()).containsEntry("key", "value"); + assertThat(result.getContent()).containsEntry("key", "value"); } @Test // #1157 @@ -608,22 +610,6 @@ class Jackson2HalIntegrationTest { assertThat(result.getContent().name).isEqualTo("Dave"); } - @Test // #1352 - void rendersMapWithoutDuplicateEntries() throws JsonProcessingException { - - Map map = new TreeMap<>(); - map.put("key", "value"); - map.put("key2", "value2"); - - EntityModel> entityModel = EntityModel.of(map); - entityModel.add(Link.of("http://example.com")); - - String serialized = mapper.writeValueAsString(entityModel); - - assertThat(serialized) - .isEqualTo("{\"_links\":{\"self\":{\"href\":\"http://example.com\"}},\"key\":\"value\",\"key2\":\"value2\"}"); - } - @Relation(collectionRelation = "someSample") static class SomeSample { @JsonProperty String name;