9

My entity :

public class User {

    private Integer id;
    private String mail;
    private boolean enabled;

    // getters and setters
}

File test.json (response from REST webservice) :

{
 "_embedded" : {
  "users" : [ {
    "id" : 1,
    "mail" : "admin@admin.com",
    "enabled" : true,
    "_links" : {
      "self" : {
        "href" : "http://localhost:8080/api/users/1"
      }
    }
  } ]
 }
}

And my test class :

public class TestJson {

    private InputStream is;
    private ObjectMapper mapper;

    @Before
    public void before() {
        mapper = new ObjectMapper();
        mapper.registerModule(new Jackson2HalModule());
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

        is = TestJson.class.getResourceAsStream("/test.json");
    }

    @After
    public void after() throws IOException {
        is.close();
    }

    @Test
    public void test() throws IOException {
        PagedResources<Resource<User>> paged = mapper.readValue(is, new TypeReference<PagedResources<Resource<User>>>() {});
        Assert.assertNotNull(paged.getContent().iterator().next().getContent().getId());
    }

    @Test
    public void testResource() throws IOException {
        PagedResources<User> paged = mapper.readValue(is, new TypeReference<PagedResources<User>>() {});
        Assert.assertNotNull(paged.getContent().iterator().next().getId());
    }
}

The second test passes but not the first. I don't understand because the id property in the user is the only one missing (mail and enabled properties are not empty)...

What do I have to do to fix it ? Is it a bug in Jackson or Spring Jackson2HalModule ?

You can reproduce by cloning my spring-hateoas fork repository and launching unit tests.

mfalaize
  • 310
  • 2
  • 9

3 Answers3

13

Actually, it was due to the Resource class which is built to wrap the content of your bean. The content property is annotated by @JsonUnwrapped so that the Resource class can map your bean in this property whereas in the json, bean properties are at the same level as _links property. With this annotation, it is possible to have property name conflict with the wrapper and the inner bean. It is exactly the case here because Resource class has an id property inherited from the ResourceSupport class, and this property is sadly annotated by @JsonIgnore.

There is a workaround for this issue. You can create a new MixIn class inherited from the ResourceSupportMixin class and override the getId() method with @JsonIgnore(false) annotation :

public abstract class IdResourceSupportMixin extends ResourceSupportMixin {

    @Override
    @JsonIgnore(false)
    public abstract Link getId();
}

Then you just have to add your IdResourceSupportMixin class to your ObjectMapper :

mapper.addMixInAnnotations(ResourceSupport.class, IdResourceSupportMixin.class);

It should solve the problem.

mfalaize
  • 310
  • 2
  • 9
  • 1
    same problem with `spring-hateoas` and `spring-boot`, so I had to rename the `id` to something else – cahen Jul 14 '15 at 16:46
  • It was a year ago but if I remember correctly, it actually doesn't work because of a compilation error I think... The only one solution I found is cahen's one. You have to rename your id property to something else. – mfalaize Oct 07 '16 at 12:42
  • This works for me: `objectMapper.addMixIn(Resource.class, IdResourceSupportMixIn.class);` - with jackson 2.8.8 and spring-hateoas 0.28, and rest template. But it breaks `response.getBody().getId()` – František Hartman Aug 18 '17 at 11:40
  • 1
    ResourceSupportMixin is package private. We can not extend it. But I have found solution. Use this. ``` public abstract class IdResourceSupportMixin extends ResourceSupport { @Override @JsonIgnore(false) public abstract Link getId(); @Override @XmlElement(name = "link") @JsonProperty("_links") @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonSerialize(using = Jackson2HalModule.HalLinkListSerializer.class) @JsonDeserialize(using = Jackson2HalModule.HalLinkListDeserializer.class) public abstract List getLinks(); } ``` – hurelhuyag Apr 19 '19 at 04:51
1

With this code you find all @Entity beans an change the config to expose Id value:

 import java.util.LinkedList;
 import java.util.List;

 import javax.persistence.Entity;

 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
 import org.springframework.core.type.filter.AnnotationTypeFilter;
 import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
 import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter;
 import org.springframework.stereotype.Component;

 import com.rvillalba.exampleApiHateoas.entity.Example;

 import lombok.extern.slf4j.Slf4j;

 @Component
 @Slf4j
 public class SpringDataRestCustomization extends RepositoryRestConfigurerAdapter {

     @Override
     public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
         listMatchingClasses(Entity.class).forEach(entity -> config.exposeIdsFor(entity));
     }

     public List<Class> listMatchingClasses(Class annotationClass) {
         List<Class> classes = new LinkedList<Class>();
         ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(true);
         scanner.addIncludeFilter(new AnnotationTypeFilter(annotationClass));
         for (BeanDefinition bd : scanner.findCandidateComponents(Example.class.getPackage().getName())) {
             try {
                 classes.add(Class.forName(bd.getBeanClassName()));
             } catch (ClassNotFoundException e) {
                 log.error("listMatchingClasses problem", e);
             }
         }
         return classes;
     }

 }
  • This solution worked for me. Thanks @Raul Villalba. I replaced the `Example` class with my `BaseEntity` where I defined `private Long id` and it worked. I also changed the class to `public class SpringDataRestCustomization implements RepositoryRestConfigurer` because `RepositoryRestConfigurerAdapter` is deprecated. https://docs.spring.io/spring-data/rest/docs/3.3.1.RELEASE/api/org/springframework/data/rest/webmvc/config/RepositoryRestConfigurerAdapter.html – biniam Jun 08 '21 at 10:33
0

This worked for me:

public class User extends ResourceSupport {

    @JsonIgnore(false)
    private Integer id;
    private String mail;
    private boolean enabled;

    // getters and setters
}

Also, change your http client to return PagedResources <User> instead of PagedResources<Resource<User>>

ItamarG3
  • 4,092
  • 6
  • 31
  • 44
  • How did you have getter for `Integer id` field if ResourceSupport has `public Link getId()` method? – Mikhail Kopylov Mar 14 '19 at 11:25
  • @MikhailKopylov You are right. @Will Franco - this solution doesn't work. This is the error that I see. ```'getId()' in 'com.example.entity.User' clashes with 'getId()' in 'org.springframework.hateoas.ResourceSupport'; attempting to use incompatible return type``` – biniam Jun 08 '21 at 10:21