Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Hateoas ControllerLinkBuilder adds null fields

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);
}
like image 885
daniu Avatar asked Apr 02 '18 16:04

daniu


4 Answers

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).

like image 53
daniu Avatar answered Oct 24 '22 09:10

daniu


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

like image 35
java-addict301 Avatar answered Oct 24 '22 09:10

java-addict301


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.

like image 2
semiintel Avatar answered Oct 24 '22 09:10

semiintel


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.

like image 2
Michael Hegner Avatar answered Oct 24 '22 11:10

Michael Hegner