Spring Hateoas ControllerLinkBuilder adds null fields
Asked Answered
C

5

13

I'm following a tutorial on Spring REST and am trying to add HATEOAS links to my Controller results.

I have a simple User class and a CRUD controller for it.

class User {
    private int id;
    private String name;
    private LocalDate birthdate;
    // and getters/setters
}

Service:

@Component
class UserService {
    private static List<User> users = new ArrayList<>();
    List<User> findAll() {
        return Collections.unmodifiableList(users);
    }
    public Optional<User> findById(int id) {
        return users.stream().filter(u -> u.getId() == id).findFirst();
    }
    // and add and delete methods of course, but not important here
}

Everything works fine except in my Controller, I want to add links from the all user list to the single users:

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;

@RestController
public class UserController {
    @Autowired
    private UserService userService;
    @GetMapping("/users")
    public List<Resource<User>> getAllUsers() {
        List<Resource<User>> userResources = userService.findAll().stream()
            .map(u -> new Resource<>(u, linkToSingleUser(u)))
            .collect(Collectors.toList());
        return userResources;
    }
    Link linkToSingleUser(User user) {
        return linkTo(methodOn(UserController.class)
                      .getById(user.getId()))
                      .withSelfRel();
    }

so that for every User in the result list, a link to the user itself is added.

The link itself is created fine, but there are superfluous entries in the resulting JSON:

[
    {
        "id": 1,
        "name": "Adam",
        "birthdate": "2018-04-02",
        "links": [
            {
                "rel": "self",
                "href": "http://localhost:8080/users/1",
                "hreflang": null,
                "media": null,
                "title": null,
                "type": null,
                "deprecation": null
            }
        ]
    }
]

Where do the fields with null value (hreflang, media etc) come from and why are they added? Is there a way to get rid of them?

They do not appear when building a link to the all users list:

@GetMapping("/users/{id}")
public Resource<User> getById(@PathVariable("id") int id) {
    final User user = userService.findById(id)
                                 .orElseThrow(() -> new UserNotFoundException(id));
    Link linkToAll = linkTo(methodOn(UserController.class)
                            .getAllUsers())
                            .withRel("all-users");
    return new Resource<User>(user, linkToAll);
}
Cowl answered 2/4, 2018 at 16:57 Comment(0)
C
9

For further reference in case anyone else stumbles upon this and I figured it out: I added an entry to application.properties, namely

spring.jackson.default-property-inclusion=NON_NULL

Why this was necessary for the Link objects, but not for the User I don't know (and haven't diven in deeper).

Cowl answered 20/4, 2018 at 13:59 Comment(7)
Thank you. This worked for me as well. However, I have an application.yml The solution looks like this: spring: jackson: default-property-inclusion: NON_NULLGut
some ambiguity here, I tried adding this property to no avail, any other solutions that you looked into?Cumulous
This is not a good solution IMO. This will disable serialization of null values across the board, when you may want them in other json structures. This is equivalent to trying to remove a few dead corn stalks by burning the field.Melcher
@Melcher Yeah it's not perfect, but I've yet come across something perfect. Your point is valid, but as long as you're aware of the effect of this workaround, you can work around that as well (can't you annotate class fields with "always serialize"?)Cowl
@Cowl That's a fair point. After spending much time digging into this issue though, I think I've found the correct solution (I've posted an answer below). Let me know if this helps your situation like it did mine.Melcher
I am also wondering about alternative solutions. This affects the whole project, which doesn't seem optimal.Linsk
This solution is not working for me. I have added the property in application.propertiesScherle
M
3

As semiintel alluded to, the problem is that your List return object is not a valid HAL type:

public List<Resource<User>> getAllUsers()

This can easily be addressed by changing the return type to Resources like this:

public Resources<Resource<User>> getAllUsers()

You can then simply wrap your Resource<> List in a Resources<> object by changing your return statement from:

return userResources;

to:

return new Resources<>(userResources)

Then you should get the proper links serialization like you do for your single object.

This method is courtesy of this very helpful article for addressing this issue:

https://www.logicbig.com/tutorials/spring-framework/spring-hateoas/multiple-link-relations.html

Melcher answered 30/1, 2019 at 13:54 Comment(0)
B
2

As per Spring HATEOAS #493 "As your top level structure is not a valid HAL structure hence bypassing the HAL serializers."

to resolve the issue use:

return new Resources<>(assembler.toResources(sourceList));

This will be resolved in a future versions of Spring HATEOAS where toResources(Iterable<>) returns Resources or by using the SimpleResourceAssembler.

Bolognese answered 6/9, 2018 at 14:2 Comment(2)
So with sourceList, do you refer to the List<User> returned by the service or to the List<Resource<User>> which contains the links I create for each user? I assume the former, but I do want the individual links... and I don't see how I'd add them before I can provide a SimpleResourceAssembler. Thanks though.Cowl
For those that don't want to deal with creating assemblers, I have posted an answer below that also returns a Resources object (in an easier way in my opinion).Melcher
D
2

To avoid the additional attributes in links as "hreflang": null, "media": null, "title": null, ..., I built a wrapper class like:

@JsonInclude(JsonInclude.Include.NON_NULL)
public class OurCustomDtoLink extends Link {
    public OurCustomDtoLink(Link link) {
        super(link.getHref(), link.getRel());
    }
}

Then I extended the org.springframework.hateoas.Resource to overwrite add(Link link):

public class OurCustomDtoResource<T extends OurCustomDto> extends Resource<T> {
    @Override
    public void add(Link link) {
        super.add(new OurCustomDtoLink(link));
    }

}

Then I extended the org.springframework.data.web.PagedResourcesAssembler to fix the paging links as following:

public class OurCustomDtoPagedResourceAssembler<T> extends PagedResourcesAssembler<T> {
    public OurCustomDtoPagedResourceAssembler(HateoasPageableHandlerMethodArgumentResolver resolver, UriComponents baseUri) {
        super(resolver, baseUri);
    }

    @Override
    public PagedResources<Resource<T>> toResource(Page<T> page, Link selfLink) {
        PagedResources<Resource<T>> resources = super.toResource(page, new OurCustomDtoLink(selfLink));
        exchangeLinks(resources);
        return resources;
    }

    @Override
    public <R extends ResourceSupport> PagedResources<R> toResource(Page<T> page, ResourceAssembler<T, R> assembler, Link link) {
        PagedResources<R> resources = super.toResource(page, assembler, new OurCustomDtoLink(link));
        exchangeLinks(resources);
        return resources;
    }

    @Override
    public PagedResources<?> toEmptyResource(Page<?> page, Class<?> type, Link link) {
        PagedResources<?> resources = super.toEmptyResource(page, type, new OurCustomDtoLink(link));
        exchangeLinks(resources);
        return resources;
    }

    private void exchangeLinks(PagedResources<?> resources) {
        List<Link> temp = new ArrayList<>(resources.getLinks()).stream()
            .map(OurCustomDtoLink::new)
            .collect(Collectors.toList());
        resources.removeLinks();
        resources.add(temp);
    }
}

But better solution would be producing correct media type org.springframework.hateoas.MediaTypes.HAL_JSON_VALUE. In my case we produced org.springframework.http.MediaType.APPLICATION_JSON_VALUE, what is not correct, but it would break the clients if we would change it afterwards.

Directional answered 20/6, 2019 at 11:20 Comment(2)
can't seem to find WsDto anywhere. is this your own type?Linsk
Hi James, thank you for your message. Maybe it was not clear, but WsDto is just any custom dto type. I tried to make it more clear in my answer now.Directional
C
0

Add this dependency in your pom. Check this link: https://www.baeldung.com/spring-rest-hal

<dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-rest-hal-browser</artifactId>
</dependency>

It will change your response like this.

"_links": {
    "next": {
        "href": "http://localhost:8082/mbill/user/listUser?extra=ok&page=11"
    }
}
Chromatic answered 23/4, 2019 at 15:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.