skip to Main Content

I am using a Spring Boot application.

I have a method in my controller that returns some Resources:

    @ResponseBody
    @Transactional(rollbackFor = Exception.class)
    @GetMapping(value="data/{itemId}/items", produces="application/json")
    public Resources<DataExcerpt> listMyData(@PathVariable("debateId") UUID debateId)){

       List<DataExcerpt> dataExcerpts = dataService
                .listMyData(id)
                .stream()
                .map(d -> this.projectionFactory.createProjection(DataExcerpt.class, d))
                .collect(Collectors.toList());
        return new Resources<>(dataExcerpts);
    }

This returns something in the form of:

{
  "_embedded" : {
    "items" : [ {
      "position" : {
        "name" : "Oui",
        "id" : "325cd3b7-1666-4c44-a55f-1e7cc936a3aa",
        "color" : "#51B63D",
        "usedForPositionType" : "FOR_CON"
      },
      "id" : "5aa48cfb-5505-43b6-b0a9-5481c895e2bf",
      "item" : [ {
        "index" : 0,
        "id" : "43c2dcd0-6bdb-43b0-be97-2a40b99bc753",
        "description" : {
          "id" : "021ad7cd-4bf1-4dce-9ea7-10980440a049",
          "title" : "Item description",
          "modificationCount" : 0
        }
      } ],
      "title" : "Item title",
      "originalMaker" : {
        "username" : "jeremieca",
        "id" : "cfae1a04-cb00-4ad4-b4e8-6971eff64807",
        "avatarUrl" : "user-16",
        "_links" : {
          "self" : {
            "href" : "http://some-api-link"
          }
        }
      },
      "itemState" : {
        "itemState" : "LIVE",
      },
      "opinionImprovements" : [ ],
      "sourcesJson" : [ ],
      "makers" : [ {
        "username" : "jeremieca",
        "id" : "cfae1a04-cb00-4ad4-b4e8-6971eff64807",
        "avatarUrl" : "user-16",
        "_links" : {
          "self" : {
            "href" : "http://some-api-link"
          }
        }
      } ],
      "modificationsCounter" : 1,
      "originalBuyer" : "fd9b68f9-7c0c-4120-869c-c63d1680e7f0",
      "updateTrace" : {
        "createdOn" : "2020-05-25T08:12:56.846+0000",
        "createdBy" : "cfae1a04-cb00-4ad4-b4e8-6971eff64807",
        "updatedOn" : "2020-05-25T08:12:56.845+0000",
        "updatedBy" : "cfae1a04-cb00-4ad4-b4e8-6971eff64807"
      },
      "_links" : {
        "self" : {
          "href" : "some-api-link",
          "templated" : true
        },
        "newEditions" : {
          "href" : "some-api-link",
          "templated" : true
        },
        "makers" : {
          "href" : "http://some-api-link"
        },
        "originalMaker" : {
          "href" : "http://some-api-link"
        }
      }
    } ]
  }
}

On the other end, I also want to cache these sort of answers inside Redis to avoid running the whole process every time. To do that, I am using Jackson’s ObjectMapper to convert my Resources to a string

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValueAsString(controller.listMyData(id)); // the same function as above

writeValueAsString output’s structure is different from the first one:

"{content: [...], _links: []}"

So, when I return from my API with the cache content, the structure is not the same from the structure the controller sends me without the cache.

Why is that?
Is Jackson not able to correctly write as string the Resources Hateoas structures?
Am I missing something?

EDIT:

Here is the Resources.class:

package org.springframework.hateoas;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import org.springframework.util.Assert;

@XmlRootElement(name = "entities")
public class Resources<T> extends ResourceSupport implements Iterable<T> {
    private final Collection<T> content;

    protected Resources() {
        this(new ArrayList(), (Link[])());
    }

    public Resources(Iterable<T> content, Link... links) {
        this(content, (Iterable) Arrays.asList(links));
    }

    public Resources(Iterable<T> content, Iterable<Link> links) {
        Assert.notNull(content, "Content must not be null!");
        this.content = new ArrayList();
        Iterator var3 = content.iterator();

        while (var3.hasNext()) {
            T element = var3.next();
            this.content.add(element);
        }

        this.add(links);
    }

    public static <T extends Resource<S>, S> Resources<T> wrap(Iterable<S> content) {
        Assert.notNull(content, "Content must not be null!");
        ArrayList<T> resources = new ArrayList();
        Iterator var2 = content.iterator();

        while (var2.hasNext()) {
            S element = var2.next();
            resources.add(new Resource(element, new Link[0]));
        }

        return new Resources(resources, new Link[0]);
    }

    @XmlAnyElement
    @XmlElementWrapper
    @JsonProperty("content")
    public Collection<T> getContent() {
        return Collections.unmodifiableCollection(this.content);
    }

    public Iterator<T> iterator() {
        return this.content.iterator();
    }

    public String toString() {
        return String.format("Resources { content: %s, %s }", this.getContent(), super.toString());
    }

    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        } else if (obj != null && obj.getClass().equals(this.getClass())) {
            Resources<?> that = (Resources) obj;
            boolean contentEqual = this.content == null ? that.content == null : this.content.equals(that.content);
            return contentEqual ? super.equals(obj) : false;
        } else {
            return false;
        }
    }

    public int hashCode() {
        int result = super.hashCode();
        result += this.content == null ? 0 : 17 * this.content.hashCode();
        return result;
    }
}

Thank you.

2

Answers


  1. The reason is that when Spring configures your MVC or Spring Boot application with HATEOAS, among other things, it will configure custom Jackson modules for handling the serialization and deserialization process of the Resources class and the rest of the object model exposed by the API.

    If you want to obtain a similar result, you can do something like the following:

    import org.springframework.hateoas.mediatype.hal.Jackson2HalModule;
    
    // ...
    
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(new Jackson2HalModule());
    objectMapper.writeValueAsString(controller.listMyData(id));
    
    
    Login or Signup to reply.
  2. My advice would be to provide POJOs, extending hateoasResourceSupport, through which the (de-)serialization would go through, eg.

    ResourcesJson (the root element)

    public class ResourcesJson extends ResourceSupport {
        @JsonProperty("_embedded")
        private ResourcesEmbeddedListJson  embedded;
    
        //getters and setters
    }
    

    Embedded "wrapper"

    public class ResourcesEmbeddedListJson extends ResourceSupport {
        private Collection<T> content;
    
        //getters and setters
    }
    

    or if you want to make it less ugly, there’s this org.springframework.hateoas.client.Traverson component.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search