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 formapplication/vnd.service.entity.v1.hal+json
againstapplication/*hal+json
.MimeType
will recognize vendor-specific HAL media-types of the formapplication/vnd.service.entity.v1+hal+json
againstapplication/*+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?
hal
when you have a more specific media type? It doesn't make sense asvnd.service.entity.v1
would be based on theHAL
format per definition. What advantages do you expect by addinghal
to the media type? – Kathavnd.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. – Petapplication/hal+json
with a profile ofvnd.service.entity.v1
semantically different fromvnd.service.entity.v2
? Would it be interpreted as such? – Petvnd.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. – Kathaprofile
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. – Petapplication/hal+json
". – PetAccept
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-mvc – Xantha