Cannot do HAL+JSON Level 3 RESTful API with Spring HATEOAS due to lack of clarity surrounding HAL+JSON media-type
Asked Answered
P

1

13

Level 3 RESTful API's feature custom media-types like application/vnd.service.entity.v1+json, for example. In my case I am using HAL to provide links between related resources in my JSON.

I'm not clear on the correct format for a custom media-type that uses HAL+JSON. What I have currently, looks like application/vnd.service.entity.v1.hal+json. I initially went with application/vnd.service.entity.v1+hal+json, but the +hal suffix is not registered and therefore violates section 4.2.8 of RFC6838.

Now Spring HATEOAS supports links in JSON out of the box but for HAL-JSON specifically, you need to use @EnableHypermediaSupport(type=EnableHypermediaSupport.HypermediaType.HAL). In my case, since I am using Spring Boot, I attach this to my initializer class (i.e., the one that extends SpringBootServletInitializer). But Spring Boot will not recognize my custom media-types out of the box. So for that, I had to figure out how to let it know that it needs to use the HAL object-mapper for media-types of the form application/vnd.service.entity.v1.hal+json.

For my first attempt, I added the following to my Spring Boot initializer:

@Bean
public HttpMessageConverters customConverters() {
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    converter.setSupportedMediaTypes(Arrays.asList(
            new MediaType("application", "json", Charset.defaultCharset()),
            new MediaType("application", "*+json", Charset.defaultCharset()),
            new MediaType("application", "hal+json"),
            new MediaType("application", "*hal+json")
    ));

    CurieProvider curieProvider = getCurieProvider(beanFactory);
    RelProvider relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class);
    ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);

    halObjectMapper.registerModule(new Jackson2HalModule());
    halObjectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider));

    converter.setObjectMapper(halObjectMapper);

    return new HttpMessageConverters(converter);
}

This worked and I was getting the links back in proper HAL format. However, this was coincidental. This is because the actual media-type that ends up being reported as "compatible" with application/vnd.service.entity.v1.hal+json is *+json; it doesn't recognize it against application/*hal+json (see later for explanation). I didn't like this solution since it was polluting the existing JSON converter with HAL concerns. So, I made a different solution like so:

@Configuration
public class ApplicationConfiguration {

    private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";

    @Autowired
    private BeanFactory beanFactory;

    @Bean
    public HttpMessageConverters customConverters() {
        return new HttpMessageConverters(new HalMappingJackson2HttpMessageConverter());
    }

    private class HalMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
        public HalMappingJackson2HttpMessageConverter() {
            setSupportedMediaTypes(Arrays.asList(
                new MediaType("application", "hal+json"),
                new MediaType("application", "*hal+json")
            ));

            ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
            setObjectMapper(halObjectMapper);
        }
    }
}

This solution does not work; I end up getting links in my JSON that don't conform to HAL. This is because application/vnd.service.entity.v1.hal+json is not recognized by application/*hal+json. The reason this happens is that MimeType, which checks for media-type compatibility, only recognizes media-types that start with *+ as valid wild-card media-types for subtypes (e.g., application/*+json). This is why the first solution worked (coincidentally).

So there are two problems here:

  • MimeType will never recognize vendor-specific HAL media-types of the form application/vnd.service.entity.v1.hal+json against application/*hal+json.
  • MimeType will recognize vendor-specific HAL media-types of the form application/vnd.service.entity.v1+hal+json against application/*+hal+json, however these are invalid mimetypes as per section 4.2.8 of RFC6838.

It seems like the only right way would be if +hal is recognized as a valid suffix, in which case the second option above would be fine. Otherwise there is no way any other kind of wild-card media-type could specifically recognize vendor-specific HAL media-types. The only option would be to override the existing JSON message converter with HAL concerns (see first solution).

Another workaround for now would be to specify every custom media-type you are using, when creating the list of supported media-types for the message converter. That is:

@Configuration
public class ApplicationConfiguration {

    private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";

    @Autowired
    private BeanFactory beanFactory;

    @Bean
    public HttpMessageConverters customConverters() {
        return new HttpMessageConverters(new HalMappingJackson2HttpMessageConverter());
    }

    private class HalMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
        public HalMappingJackson2HttpMessageConverter() {
            setSupportedMediaTypes(Arrays.asList(
                new MediaType("application", "hal+json"),
                new MediaType("application", "vnd.service.entity.v1.hal+json"),
                new MediaType("application", "vnd.service.another-entity.v1.hal+json"),
                new MediaType("application", "vnd.service.one-more-entity.v1.hal+json")                       
            ));

            ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
            setObjectMapper(halObjectMapper);
        }
    }
}

This has the benefit of not polluting the existing JSON converter, but seems less than elegant. Does anyone know the right solution for this? Am I going about this completely wrong?

Pet answered 15/12, 2014 at 23:6 Comment(14)
Instead of a custom media type, why don't you use HAL with a profile of your resources instead?Xantha
Why would you include hal when you have a more specific media type? It doesn't make sense as vnd.service.entity.v1 would be based on the HAL format per definition. What advantages do you expect by adding hal to the media type?Katha
@JonathanW Could you explain that a bit more? I'm not familiar with using profiles.Pet
@zeroflagL So are you saying that there's no need to specify that vnd.service.entity.v1 support HAL from just looking at the media-type? That it should simply be specified in the documentation for that media-type? That idea is certainly appealing. I guess I wanted to make it clear that it was HAL+JSON with additional semantics, from looking at the media-type.Pet
@VivinPaliath profile is a formal parameter to the application/hal+json media type, so it can be expressed without issue.Xantha
@JonathanW Thanks! A few more questions: can it be used for content-negotiation? Is application/hal+json with a profile of vnd.service.entity.v1 semantically different from vnd.service.entity.v2? Would it be interpreted as such?Pet
Yes. Let's assume that you planned to deliver your data alternatively with and without HAL support. Maybe then vnd.service.entity.v1.hal is worth to be considered to differentiate between those two. HAL may be(come) a standard but it's more a convention than a format after all. It doesn't say much about your resource.Katha
@zeroflagL It seems that profile is expected to be a URI. So should the URI contain a schema of the JSON? I'm more concerned with the content-negotiation aspect.Pet
I was referring to my own comment, not to profiles :)Katha
@zeroflagL Oops got confused :) What you said makes sense. I guess I wanted a way to say "all of these are more-specific subtypes of application/hal+json".Pet
@VivinPaliath Assuming that the request provides the profile parameter in the Accept header, then yes. :) If you mean "can Spring MVC support that?" then yes... but it may take a little configuring. See spring.io/blog/2013/05/11/content-negotiation-using-spring-mvcXantha
In terms of a URL for the profile...It certainly doesn't have to point to a schema, but it certainly COULD. I personally favor human readable documentation, because schema is never enough to communicate the interface contractXantha
Alternatively it could solely be a URI and not point to anything when dereferenced. Much like (dare I say) an XML namespace.Xantha
actually the trick is to use content negotiation on the profile URL too. So if your request to the profile URL has Accept: application/some.schema.type, you get a response in that schema (if available) and if you ask for text/html you get human readable html doc about that resource profile. Also when requesting the resource itself you use profile parameter. IE Accept: application/hal+json; profile=URIUpdo
F
4

Although this question is a litte bit old, I recently stumbled upon the same problem so I wanted to give my 2 cents to this topic.

I think the problem here is the understanding of HAL regarding JSON. As you already pointed out here, all HAL is JSON but not all JSON is HAL. The difference between both is, from my understanding, that HAL defines some conventions to the semantics/structure, like telling you that behind an attribute like _links you'll find some links, whereas JSON just defines the format like key: [value] (as @zeroflagL already mentioned)

This is the reason, why the media type is called application/hal+json. It basically says it's the HAL style/semantics in the JSON format. This is also the reason that there exists a media type application/hal+xml (source ).

Now with a vendor specific media type, you define your own semantics and so your replacing the hal in application/hal+json and don't extend it.

If I understand you right, you basically want to say that you have a custom media type that uses the HAL style for it's JSON formatting. (This way, a client could use some HAL library to easily parse your JSON.)

So, at the end I think you basically have to decide wether you want to differentiate between JSON and HAL-based JSON and wether your API should provide one of these or both.

If you want to provide both, you'll have to define two different media types vnd.service.entity.v1.hal+json AND vnd.service.entity.v1+json. For the vnd.service.entity.v1.hal+json media type you then have to add your customized MappingJackson2HttpMessageConverter that uses the _halObjectMapper to return HAL-based JSON whereas the +json media type is supported by default returning your resource in good old JSON.

If you always want to provide HAL-based JSON, you have to enable HAL as the default JSON-Media type (for instance, by adding a customized MappingJackson2HttpMessageConverter that supports the +json media type and uses the _halObjectMapper mentioned before), so every request to application/vnd.service.entity.v1+json is handled by this converter returning HAL-based JSON.

From my opinion I think the right way is to only differentiate between JSON and other formats like XML and in your media type documentation you'd say, that your JSON is HAL-inspired in a way that clients can use HAL libs to parse the responses.


EDIT:

To bypass the problem that you'll have to add each vendor specific media type separately, you could override the isCompatibleWith method of the media type you're adding to your custom MappingJackson2HttpMessageConverter

converter.setSupportedMediaTypes(Arrays.asList(
            new MediaType("application", "doesntmatter") {
                @Override
                public boolean isCompatibleWith(final MediaType other) {
                    if (other == null) {
                        return false;
                    }
                    else if (other.getSubtype().startsWith("vnd.") && other.getSubtype().endsWith("+json")) {
                        return true;
                    }
                    return super.isCompatibleWith(other);
                }
            }
));
Felten answered 16/4, 2015 at 13:36 Comment(1)
I like the addition of the new media-type to support my vendor-specific media-types. I more or less arrived at the same conclusion - HAL is just additional semantics on top of JSON, and a custom media-type that uses HAL is your own semantics on top of that. I guess ultimately it just comes down to documenting the media-types and making it clear up front that every resource-representation returned by the API uses HAL semantics. On the code side, providing a custom media-type to recognize your own media-types also makes sense.Pet

© 2022 - 2024 — McMap. All rights reserved.