Search code examples
springrestspring-mvchateoasspring-hateoas

Spring HATEOAS: Only show links when paging without serializing the resource/entity


According to the Spring HATEOAS guide, a list of resources is serialized in a way that each resource is shown with its content and its links:

{
    "content": [ {
        "price": 499.00,
        "description": "Apple tablet device",
        "name": "iPad",
        "links": [ {
            "rel": "self",
            "href": "http://localhost:8080/product/1"
        } ],
        "attributes": {
            "connector": "socket"
        }
    }, {
        "price": 49.00,
        "description": "Dock for iPhone/iPad",
        "name": "Dock",
        "links": [ {
            "rel": "self",
            "href": "http://localhost:8080/product/3"
        } ],
        "attributes": {
            "connector": "plug"
        }
    } ],
    "links": [ {
        "rel": "product.search",
        "href": "http://localhost:8080/product/search"
    } ]
}   

In case of large data structures, I think it would be better to only provide the links to the resources and not the resources itself like this (especially when paging):

{
    "_links": {
      "items": [{
          "href": "http://localhost:8080/product/1"
      },{
          "href": "http://localhost:8080/product/3"
      }]
    }
}

Aside from the fact that this would decrease the size of the transferred bytes, this is also suggested by the HAL specification. I'm currently doing it this way

@RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity root(Pageable pageable, final PagedResourcesAssembler<Entity> assembler) {
    Page<Entity> entities = entityRepository.findAll(pageable);

    PagedResources<Resource> paged = assembler.toResource(entities,
            EntityResourceAssembler.getInstance());

    Collection<Resource> resources = paged.getContent();
    ResourceSupport support = new ResourceSupport();

    for (Resource r : resources) {
        Link selfLink = r.getLink(Link.REL_SELF);
        support.add(new Link(selfLink.getHref(), "items"));
    }

    return new ResponseEntity<ResourceSupport>(support, HttpStatus.OK);
}

But this is a bit ugly as I "manually" need to fetch the self link from the resources. Is there a better/smarter way to achieve what I want?


Solution

  • I'm going to ignore why you don't want to embed sub-resources (and thus have less request-response cycles, the more expensive part of a client request) and answer your question as best I can.

    You cannot use the built in PagedResourceAssembler to do what you want. Instead derive something from ResourceSupport and just add the links yourself. We do this in certain situations where we only want to link instead of embedding the resource (because the resource is not available as HAL)

    public class PageResource extends ResourceSupport {
    
        @XmlAttribute(name = "page")
        @JsonProperty("page")
        private PageMeta pageMeta;
    
        public PageResource() {
            this.pageMeta = new PageMeta();
        }
    
        public PageMeta getPageMeta() {
            return pageMeta;
        }
    
        public void setPageMeta(PageMeta pageMeta) {
            this.pageMeta = pageMeta;
        }
    
        public static class PageMeta {
            //Number of resources on this page.
            @XmlAttribute @JsonProperty private long size;
            //Total number of matching resources
            @XmlAttribute @JsonProperty private long totalElements;
            //Total number of page
            @XmlAttribute @JsonProperty private long totalPages;
            //Current page number
            @XmlAttribute @JsonProperty private long number;
    
            public PageMeta() {
            }
    
            public PageMeta(long size, long totalElements, long totalPages, long number) {
                this.size = size;
                this.totalElements = totalElements;
                this.totalPages = totalPages;
                this.number = number;
            }
    
            public long getSize() {
                return size;
            }
    
            public void setSize(long size) {
                this.size = size;
            }
    
            public long getTotalElements() {
                return totalElements;
            }
    
            public void setTotalElements(long totalElements) {
                this.totalElements = totalElements;
            }
    
            public long getTotalPages() {
                return totalPages;
            }
    
            public void setTotalPages(long totalPages) {
                this.totalPages = totalPages;
            }
    
            public long getNumber() {
                return number;
            }
    
            public void setNumber(long number) {
                this.number = number;
            }
        }
    }
    

    is a very simple implementation, then in your controller you are responsible for adding next and prev links, along with the item links.

    At this point you could build your own PagedResourceAssembler that only does links instead of embedding sub-resources.

    Small aside...you should consider adding the links as REL "item" and not "items". It's tempting to think of the collection of links and give the json field key the plural name, but remember the HAL spec just uses the field key as the link relationship.

    Compare in html form

    <link rel="items" src="http://1"/>
    <link rel="items" src="http://2"/>
    

    to

    <link rel="item" src="http://1"/>
    <link rel="item" src="http://2"/>
    

    and it's apparent each link's target shared an item relationship to the collection, not an "items" as each link targets a single item in the collection.