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?