Spring-hateoas: Keys for HAL's _embedded document and link relations do not follow the Jackson PropertyNamingStrategy configured

Created on 28 Nov 2019  ·  19Comments  ·  Source: spring-projects/spring-hateoas

I have two beans in my configuration:

    @Bean
    public ObjectMapper getObjectMapper() {
        val objectMapper = new ObjectMapper();
        objectMapper.setPropertyNamingStrategy(SNAKE_CASE);
        objectMapper.registerModule(new AfterburnerModule());
        objectMapper.registerModule(new ProblemModule());
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        return objectMapper;
    }

    @Bean
    public Jackson2ObjectMapperBuilder getJackson2ObjectMapperBuilder() {
        val jackson2ObjectMapperBuilder = new Jackson2ObjectMapperBuilder();
        jackson2ObjectMapperBuilder.propertyNamingStrategy(SNAKE_CASE);
        return jackson2ObjectMapperBuilder;
    }

The problem is my response objects are still CAMEL_CASE so I don't think Hateaos is using the correct bean. Am I writing the correct bean to change the naming strategy so that my response is entity_models and not entityModels?

Thanks!

bug waiting for feedback configuration

All 19 comments

Would you mind providing a sample project that shows the erroneous behavior. All of Spring Data's configuration setup actually defensively uses an ObjectMapper instance present in the ApplicationContext and only falls back to a fresh instance if none is configured at all.

So I'd love to see an example which we can play with to determine what's maybe wrong with the ordering of configuration.

Hi @odrotbohm , sorry for the delay. I am working on trying to debug and dig deeper of where the object mapper changes from my custom one to a new one. Not quite there yet but will work on making the sample project today to show you the behavior.

Thanks

Hi @odrotbohm here is the repo. In the configuration#WebMvcConfiguration I have my custom mapper.

If you run the application and go http://localhost:8080/greetings you will see that entityModelList in the response and content_test. Other objects are being snake-cased but not the EntityModel.

Thank you for looking into this.

That's helpful, Chad! I'll have a look ASAP.

To summarize my immediate findings: the camel case field name you see is not backed by a property in the Jackson sense as it's a Map that we create grouping entities by their link relation. That's why the PropertyNamingStrategy is not automatically applied by Jackson.

That said, I think we can make this work by piping the keys we use through the PropertyNamingStrategy provided.

Are you proposing this solution for me to apply to my project or a change to the Hateoas library?

If you want to define custom link relations via @Relation on you entity types, those could use camel case and would actually get reflected in those map keys. That however also changes the link relations exposed by LinkRelationProvider in general.

I have a prototype of a fix locally that applies the strategy during the serialization of that map. I think wwe can roll this out with the next bugfix release.

Thinking about this a bit more I wonder whether we should actually fix this. The HAL specification defines the keys to be used in _embedded to be link relation types. Section 8.3 suggests a symmetry between the link relations used within _embedded and the ones used in _links. This leaves us with two options:

  1. Leave everything as is and resort to the point of view that if you wanted to customize the field names you'd have to resort to customize the link relations themselves via either annotations or implementing a custom LinkRelationProvider. This would guarantee consistency between the relations used in _links and _embedded.
  2. Apply the PropertyNamingStrategy used for serialization in the custom serializers and map the link relations using the strategy. This fixes Chad's request but creates the inconsistency between _links and _embedded.

Assuming that the symmetry aspect of all of this is important, we could augment the first approach by augmenting DelegatingLinkRelationProvider with the ability to register a transformer. The HalMediaTypeConfiguration could then forward the PropertyNamingStrategy defined by the ObjectMapper into such a transformer.

That said, I wonder how we'd want to treat and identify IANA issued link relations as they have to stay as is.

Input appreciated.

If you want to define custom link relations via @Relation on you entity types, those could use camel case and would actually get reflected in those map keys. That however also changes the link relations exposed by LinkRelationProvider in general.

I have a prototype of a fix locally that applies the strategy during the serialization of that map. I think wwe can roll this out with the next bugfix release.

Do you have an ETA for this bugfix? I have a merge deadline on the 13th.

@odrotbohm In response to the two options proposed. I was actually trying to make a custom LinkRelationProvider bean yesterday to solve this issue but ran into some issues. Maybe you can clarify how to properly create a custom LinkRelationProvider.

@Configuration
@RequiredArgsConstructor
@EnableHypermediaSupport(type = HAL)
public class WebMvcConfiguration implements WebMvcConfigurer {
...
        @Override
        public LinkRelation getItemResourceRelFor(Class<?> type) {
            val simpleName = type.getSimpleName();
            String typeName = "";
            for (char letter : simpleName.toCharArray()) {
                if (simpleName.indexOf(letter) == 0) {
                    typeName = typeName + Character.toLowerCase(letter);
                }
                else if (Character.isUpperCase(letter)) {
                    typeName = typeName + "_" + Character.toLowerCase(letter);
                }
                else {
                    typeName = typeName + letter;
                }
            }
            return LinkRelation.of(typeName);
        }
    }

    @Bean
    @Primary
    public LinkRelationProvider provider() {
        return new CustomLinkRelationProvider();
    }

I get the following error:

No qualifying bean of type 'org.springframework.hateoas.server.LinkRelationProvider' available: more than one 'primary' bean found among candidates: [defaultRelProvider, annotationRelProvider, _relProvider, provider]

The complete message:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'webSecurityConfiguration': Unsatisfied dependency expressed through method 'setContentNegotationStrategy' parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration': Unsatisfied dependency expressed through method 'setConfigurers' parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'hypermediaWebMvcConfigurer' defined in class path resource [org/springframework/hateoas/config/WebMvcHateoasConfiguration.class]: Unsatisfied dependency expressed through method 'hypermediaWebMvcConfigurer' parameter 1; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.hateoas.mediatype.hal.HalMediaTypeConfiguration': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.hateoas.server.LinkRelationProvider' available: more than one 'primary' bean found among candidates: [defaultRelProvider, annotationRelProvider, _relProvider, provider]

Am I correct to assume getItemResourceRelFor is the method doing the mapping between EntityModels to entityModels?

As we've just released a service release there's no real chance that we're gonna issue another one before the end of the year.

Please not make a LinkRelationProvider a primary bean. Spring HATEOAS will already register a primary one automatically picking up the ones registered by users.

If I remove the @Primary then my bean is not being used and just defaults to the one from Hateoas.

Here is my configuration now, can you spot anything wrong?

package ca.clearly.web.app.common.configuration;

import lombok.val;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.server.LinkRelationProvider;
import org.springframework.hateoas.server.core.DefaultLinkRelationProvider;

@Configuration
public class HateoasConfiguration {
    public class CustomLinkRelationProvider extends DefaultLinkRelationProvider implements LinkRelationProvider {

        @Override
        public LinkRelation getItemResourceRelFor(Class<?> type) {
            val simpleName = type.getSimpleName();
            String typeName = "";
            for (char letter : simpleName.toCharArray()) {
                if (simpleName.indexOf(letter) == 0) {
                    typeName = typeName + Character.toLowerCase(letter);
                }
                else if (Character.isUpperCase(letter)) {
                    typeName = typeName + "_" + Character.toLowerCase(letter);
                }
                else {
                    typeName = typeName + letter;
                }
            }
            return LinkRelation.of(typeName);
        }
    }

    @Bean
    public LinkRelationProvider provider() {
        return new CustomLinkRelationProvider();
    }
}

Okay I am good, adding the following solved it. I don't require any changes so if the Hateoas team doesn't feel the need to work on this then the ticket can be closed. Thank you for the help!

 @Override
        public int getOrder() {
            return Ordered.HIGHEST_PRECEDENCE;
        }

This would guarantee consistency between the relations used in _links and _embedded.

From a spec point of view, the link relation type defines the kind of relation between to resources. So there's no reason why a link relation type for an embedded resource should differ from a link relation type describing a link - the relation is semantically the same.

From a practical point of view, I'd also definitely argue in favor of consistent link relation types for _links and _embedded. We relied on this convention in our REST client to selectively optimize requests by embedding resources that were previously only linked in a represenation without having to change anything in the client (see implementation here).

In general, I'd like to point out that the Web Linking spec (RFC 8288) requires extension link relation types (meaning, link relation types that are not registered through IANA) to be URIs and the JSON-HAL spec references that RFC also in regards to _embedded. So if you want to be nitpicky, neither entityModelList nor entity_model_list are valid link relation types in the first place. But I guess in practice nobody cares about that ... 🤷‍♂️

Thanks, Marvin. That's a lot of Interesting details.

If I read the spec correctly, the links only need to be absolute URIs if they're used within a Link header. Also, the general rule seems to define either the reg-rel-type format or a URI. reg-rel-type is the format which registered relation types have to follow but that doesn't mean link relation types need to be registered or URIs. Happy to learn that I'm wrong here :). That said, HAL CURIE mechanism allows to basically interpret all link relations as URIs by producing short versions of it that are valid URIs themselves.

What's puzzling, too is that reg-rel-type only defines lower case characters to be allowed but the IANA list containing a few that use camel case.

I definitely take this as a vote for consistency here, thanks!

If I read the spec correctly, the links only need to be absolute URIs if they're used within a Link header.

😳 you're right, I never noticed that.

It seems that handling of URI/non-URI link relation types depends on the context where they are used. E.g. RFC 8288 discusses link relation types in Atom:

Atom defines extension relation types in terms of IRIs. This specification redefines them as URIs, to simplify and reduce errors in their comparison.

Interesting stuff. Does that mean, that a Media Type may define its own, non-URI link relation types? 🤔 Of course, those could only be used in messages encoded with that Media Type, but still...

For example, the HTML spec defines a couple of link relation types. Would those be valid within HTML documents even if they were not registered with the IANA?

The JSON-HAL spec defines URI link relation types as optional, but recommended

Custom link relation types (Extension Relation Types in [RFC5988]) SHOULD be URIs

I've pushed a change that applies the naming strategy to both the keys in _embedded and link relations in _links. You can opt out of this post processing via HalConfiguration.withApplyPropertyNamingStrategy(false).

I'd refrain from back-porting this to 1.0.x for now as it introduces new API and configuration options. That said, the 1.1 snapshots haven't seen too significant changes yet if you want to give them a spin. In case you're using Spring Data REST with it, you need to use 3.3.x snapshots, i.e. snapshots of the Neumann release traing (Boot configured to use spring-data-releasetrain.version with Neumann-BUILD-SNAPSHOT and spring-hateoas.version with 1.1.0.BUILD-SNAPSHOT.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

odrotbohm picture odrotbohm  ·  3Comments

onacit picture onacit  ·  6Comments

ravishrathod picture ravishrathod  ·  5Comments

odrotbohm picture odrotbohm  ·  6Comments

KenErikson picture KenErikson  ·  3Comments