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?

Community
  • 1
  • 1
Vivin Paliath
  • 94,126
  • 40
  • 223
  • 295
  • Instead of a custom media type, why don't you use HAL with a profile of your resources instead? – Jonathan W Dec 16 '14 at 00:15
  • 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? – a better oliver Dec 16 '14 at 14:09
  • @JonathanW Could you explain that a bit more? I'm not familiar with using profiles. – Vivin Paliath Dec 16 '14 at 16:49
  • @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. – Vivin Paliath Dec 16 '14 at 16:51
  • @VivinPaliath *profile* is a formal parameter to the application/hal+json media type, so it can be expressed without issue. – Jonathan W Dec 16 '14 at 17:47
  • @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? – Vivin Paliath Dec 16 '14 at 18:33
  • 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. – a better oliver Dec 16 '14 at 21:56
  • @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. – Vivin Paliath Dec 16 '14 at 22:12
  • I was referring to my own comment, not to profiles :) – a better oliver Dec 17 '14 at 08:33
  • @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`". – Vivin Paliath Dec 17 '14 at 16:20
  • @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 http://spring.io/blog/2013/05/11/content-negotiation-using-spring-mvc – Jonathan W Dec 17 '14 at 17:21
  • 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 contract – Jonathan W Dec 17 '14 at 23:51
  • Alternatively it could solely be a URI and not point to anything when dereferenced. Much like (dare I say) an XML namespace. – Jonathan W Dec 18 '14 at 01:30
  • 1
    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=URI – Chris DaMour Dec 19 '14 at 07:43

1 Answers1

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);
                }
            }
));
krinklesaurus
  • 1,606
  • 3
  • 21
  • 38
  • 2
    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. – Vivin Paliath Sep 24 '15 at 16:02