spring-projects / spring-hateoas

Spring HATEOAS - Library to support implementing representations for hyper-text driven REST web services.
https://spring.io/projects/spring-hateoas
Apache License 2.0
1.04k stars 477 forks source link

Link affordances don't survive a Jackson serialization and deserialzation #1345

Open eefalomac opened 4 years ago

eefalomac commented 4 years ago

In a REST controller test I compare an object with an added self link with the result of a HTTP transport (via TestRestTemplate). The result is not equal to the original, because the deserialized link has no affordance, where the original after adding the link has one. The reason seems to be the discrepancy between the @JsonIgnore on affordances and the inclusion of this member in the equals method. This can simply be reproduced by test case according to the following (incomplete) example:

    @Test
    public void serializeWithLink() throws JsonProcessingException {

        SomeClassWithRepresentationModel  expected =  createThisObject();
        Link selfLink = linkTo(methodOn(SomeSpringRestController.class).methodForGetRequest()).withSelfRel();
        expected.add(selfLink);

        ObjectMapper objectMapper = new ObjectMapper(); // Jackson object mapper
        String jsonSerial = objectMapper.writeValueAsString(expected);
        SomeClassWithRepresentationModel actual = objectMapper.readValue(jsonSerial, SomeClassWithRepresentationModel.class);

        Assertions.assertEquals(expected, actual);
    }

At least in my special case with a real object and a real REST controller this test fails.

odrotbohm commented 4 years ago

That's not unexpected as a plain ObjectMapper instance not aware of any media type specific serializers/deserializers will not produce proper output.

eefalomac commented 4 years ago

It also happens (as in my real case) if the ObjectMapper instance went through a configuration with

Jackson2HalModule module = new Jackson2HalModule();
objectMapper.registerModule(module);
objectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(new AnnotationLinkRelationProvider(), CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY));

In any case: what should the poor object mapper do if it finds the @JsonIgnore at the affordances member?

odrotbohm commented 4 years ago

You still seem to be lacking the registration of the Jackson2HalModule.

eefalomac commented 4 years ago

The code snippet for the the object mapper is part of the spring boot configuration within

@Configuration
public class JacksonHalConfiguration extends AbstractJackson2HttpMessageConverter {
    public JacksonHalConfiguration(ObjectMapper objectMapper) {
        super(objectMapper, MediaTypes.HAL_JSON);
        Jackson2HalModule module = new Jackson2HalModule();
        objectMapper.registerModule(module);
        objectMapper.setHandlerInstantiator(
            new Jackson2HalModule.HalHandlerInstantiator(new AnnotationLinkRelationProvider(), CurieProvider.NONE,
                                                         MessageResolver.DEFAULTS_ONLY));
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                    .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
                    .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return RepresentationModel.class.isAssignableFrom(clazz);
    }
}

If I use the object mapper instance from Spring configured with our JacksonHalConfiguration via autowiring in the test example given above (change the new ObjectMapperwith an autowired instance and use a real data object) I obtain as serialized string {"_links":{"self":{"href":"/v2/predefinedLists/countries"}},.... The created link observed in the debugger, however, has one (implicitely generated) affordance, which is lost after deserialization. If you see that something is wrong in our configuration please give me a hint how to change it. If not, I'm still wondering about private @JsonIgnore List<Affordance> affordances;

odrotbohm commented 4 years ago

It doesn't make sense to expect affordances to be deserialized as they're not serialized (hence the @JsonIgnore) in the first place. For one this is due to HAL not exposing any kinds of affordances besides the links in the first place. But even for HAL Forms, the affordances model are used to produce the hypermedia format, not to consume them. Affordances are mostly driven by server side abstractions (like controller methods) that must not matter to clients in the first place. That's why that information must not be made available to clients.

Maybe we can take a step back and you describe what you're actually trying to achieve on an API exchange level. Seems we're too deep into discussing implementation details.

eefalomac commented 4 years ago

Okay, if affordances shouldn't be serialized I want to point to the other side of my problem: the equals method of Link (in version 1.0.5 it seems to be Lombok @EqualsAndHashCode, in 1.1.1 explicitly implemented). It includes the affordances in the comparison, and we are back to my original problem/question: how to compare objects with links after a serialization-deserialization cycle, e.g. after a transport via HTTP?

odrotbohm commented 4 years ago

I'd like to re-bring up the question:

Maybe we can take a step back and you describe what you're actually trying to achieve on an API exchange level. Seems we're too deep into discussing implementation details.

eefalomac commented 4 years ago

Well, I'm not sure if I can make it clearer, but I'll try:

  1. we decided to upgrade our Spring Boot services running with version 2.1.x to 2.2.7
  2. the data objects using HATEOAS now inherit from RepresentationModel<...>
  3. in the equalsmethod of these data objects the equals of the base class, i.e. that of RepresentationModel, is included
  4. Spring REST controllers offer resources supplying such objects (as ResponseEntity<...>)
  5. in tests of these REST controllers we compare an expected object with the result from a REST response (using Spring TestRestTemplate)
  6. such tests fail if the expected object is decorated with a self link (as I tried to explain in the former comments)

in short: some Spring Boot REST controller tests fail after upgrading to 2.2

odrotbohm commented 4 years ago

I think the issue here is in 5. It doesn't make too much sense to compare a Link (or even the entire model) instance on the client to the instance prepared on the server side to render the response instead of deserializing an instance on the client and verify the attributes you expect on that, potentially by creating a fresh instance with the expected setup and comparing the two.

The instances produced by the controller are very likely to contain state that's not available on the client. Imagine a RepresentationModel implementation that's backed by a managed entity. An equals(…) comparison to a deserialized instance of that on the client wouldn't succeed either.

Most tests that we see written are either fine with using JSONPath expressions on the response ($._links.foobar.href and checking that value) or use the LinkDiscoverer abstraction obtain a Link instance via the expected rel and compare hrefs etc.

eefalomac commented 4 years ago

What would you recommend: a) rigorously change what is described in (3), i.e. skip the RepresentaionModel in the equals method, or b) only change the comparisons in the mentioned tests reflecting the special serialization circumstances?