How to force Spring HATEOAS resources to render an empty embedded array?
Asked Answered
I

4

21

I have the following controller method:

@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE, value = "session/{id}/exercises")
public ResponseEntity<Resources<Exercise>> exercises(@PathVariable("id") Long id) {

  Optional<Session> opt = sessionRepository.findWithExercises(id);
  Set<Exercise> exercises = Sets.newLinkedHashSet();

  if (opt.isPresent()) {
    exercises.addAll(opt.get().getExercises());
  }

  Link link = entityLinks.linkFor(Session.class)
                         .slash(id)
                         .slash(Constants.Rels.EXERCISES)
                         .withSelfRel();

  return ResponseEntity.ok(new Resources<>(exercises, link));
}

So basically I am trying to get the expose a Set<> of Exercise entities for a particular Session. When the exercises entity is empty however I get a JSON representation like this:

{
    "_links": {
        "self": {
            "href": "http://localhost:8080/api/sessions/2/exercises"
        }
    }
}

So basically there is no embedded entity, while something like the following would be preferrable:

{
    "_links": {
        "self": {
            "href": "http://localhost:8080/api/sessions/2/exercises"
        }
    }, 
    "_embedded": {
        "exercises": [] 
    }    
}

any idea how to enforce this?

Interfile answered 17/5, 2015 at 12:18 Comment(0)
L
20

The problem here is that without additional effort there's no way to find out that the empty collection is a collection for Exercise. Spring HATEOAS has a helper class to work around this though:

EmbeddedWrappers wrappers = new EmbeddedWrappers(false);
EmbeddedWrapper wrapper = wrappers.emptyCollectionOf(Exercise.class);
Resources<Object> resources = new Resources<>(Arrays.asList(wrapper));

An EmbeddedWrapper allows you to explicitly mark objects to be added to the Resource or Resources as embedded, potentially even manually defining the rel they should be exposed under. As you can see above the helper also allows you to add an empty collection of a given type to the _embedded clause.

Lying answered 18/5, 2015 at 7:39 Comment(7)
This loses generic type information (and it's not the only case when assembling resources). I think it should be EmbeddedWrapper<T> implements Resource<T>, but Resource is no interface, and that's the root of the problems with generics in Spring HATEOAS, in my opinion.Shaduf
For PagedResources result: List<EmbeddedWrapper> embedded = Collections.singletonList(wrapper); PagedResources pagedResources = new PagedResources(embedded, metadata, links);Thora
@ccit-spence yes, take a lookThora
I also stumbled upon this, I can make it work with using Object instead of my type for embeddedWrapper but I really don't like it. Why isn't this an option or the default? When using spring data rest an empty collection is the default as well.Blinny
@Oliver Gierke rather than this ugly workaround, why not extend the constructor instead? new Resources<>(Exercise.class, exercises, ...); is acceptable for me.Burkley
Please vote for github.com/spring-projects/spring-hateoas/issues/522 (just click the "add your reaction" button)Burkley
I am not happy with this workaround either. It requires some generics acrobatics to make the compiler happy. I find it odd that CrudRepository does include the empty _embedded json block while in the controller this workaround is needed to achieve the same result.Howe
C
3

One can use the PagedResourceAssembler::toEmptyResource() method. For example, the following works:

Page<EWebProduct> products = elasticSearchTemplate.queryForPage(query, EWebProduct.class);

if(!products.hasContent()){
            PagedResources pagedResources = pageAssembler.toEmptyResource(products, WebProductResource.class,baseLink);
            return new ResponseEntity<PagedResources<WebProductResource>>(pagedResources, HttpStatus.OK);
}

I'd bet it works with other ResourceAssemblers as well.

Countertype answered 20/1, 2018 at 0:44 Comment(0)
C
1

If you have a Page< T >, you can convert it like this:

 public static <T> PagedModel<EntityModel<T>> toModel(PagedResourcesAssembler<T> assembler,
                                                Page<T> page) {
        if (!page.isEmpty()) {
            return assembler.toModel(page);
        } else {
            // toEmptyModel renders the _embedded field (with an empty array inside)
            return (PagedModel<EntityModel<T>>) assembler.toEmptyModel(page, TenantSubscriptionResponseDto.class);
        }
    }

(You can obtain the PagedResourcesAssembler assembler by simply adding it as a parameter to the Controller method, and Spring will inject it).

Clammy answered 9/6, 2020 at 9:25 Comment(0)
C
-1

Spring by default uses Jackson parser to serialize/deserialize json. As per http://wiki.fasterxml.com/JacksonFeaturesSerialization Jackson has a feature called WRITE_EMPTY_JSON_ARRAYS and its enabled by default. Maybe WRITE_EMPTY_JSON_ARRAYS is set to false in your config. please recheck your message converters configuration.

Cao answered 17/5, 2015 at 15:18 Comment(1)
I'm pretty sure it's not this, it's not JSON array, it's an entire hierarchy of _embedded in Spring HATEOAS.Burkley

© 2022 - 2024 — McMap. All rights reserved.