spring-hateoas icon indicating copy to clipboard operation
spring-hateoas copied to clipboard

Multiple MappingJackson2HttpMessageConverter's doesn't work

Open kristianborg opened this issue 3 years ago • 1 comments
trafficstars

After updating from spring-hateoas 1.2 to 1.4, we see some unexpected behavior in our REST endpoints: requests that used to succeed get 415 Unsupported Media Type response, or the format of the json response has changed (links don't end up in the _links field and content does not end up in the _embedded field).

This changed behavior seems to be caused by the changed way in which spring-hateoas deals with HttpMessageConverters. I believe the relevant commit is: https://github.com/spring-projects/spring-hateoas/commit/c561822a4520d8cd4f920b6bea8efff0d2d120a4.

In the case of our application this fails because we have registered a second MappingJackson2HttpMessageConverter, but the code in org.springframework.hateoas.config.WebConfig processes only one of them. See https://github.com/spring-projects/spring-hateoas/blob/1.4.x/src/main/java/org/springframework/hateoas/config/WebConverters.java#L94 . Therefor we end up with one converter that can deal with spring-hateoas-specific objects and one that can't, resulting in the aforementioned change in behavior.

The thing I am unsure about is whether it is correct to have multiple MappingJackson2HttpMessageConverter's. It has served us fine, and I don't see any hints in documentation that you shouldn't do that but still, maybe there is a good reason the code in WebConverters is the way it is.

Could you advise how to proceed? If this is in fact incorrect behavior in spring-hateoas I could provide a fix in a Pull Request, but I am not sure if those are appreciated.

kristianborg avatar Dec 28 '21 08:12 kristianborg

I have encountered a similar issue due to there being multiple MappingJackson2HttpMessageConverters.

We are using spring.hateoas.use-hal-as-default-json-media-type=false for some backwards compatibility until we can switch over to using proper application/hal+json and have noticed the Jackson property spring.jackson.default-property-inclusion=NON_NULL was not being respected on the content within the RepresentationModel, resulting in a bunch of null values.

Digging through the configuration showed that a default MappingJackson2HttpMessageConverter (with default configuration) was added to the converters list and then another MappingJackson2HttpMessageConverter (with our inclusion property set) was added.

In org.springframework.hateoas.config.WebConverters, the code only augments one of the MappingJackson2HttpMessageConverter and always chooses the one missing our configuration.

		MappingJackson2HttpMessageConverter converter = converters.stream()
				.filter(it -> MappingJackson2HttpMessageConverter.class.equals(it.getClass()))
				.map(MappingJackson2HttpMessageConverter.class::cast)
				.findFirst()
				.orElseGet(() -> new MappingJackson2HttpMessageConverter(mapper));

When the controller serializes the RepresentationModel, it always selects the wrong MappingJackson2HttpMessageConverter.

edit: Digging a little more, it looks like the issue comes from how the message converter is being chosen due to there being objectMapperRegistrations on the converter. As it inspects the correctly configured MappingJackson2HttpMessageConverter it now sees the objectMapperRegistrations that were added by org.springframework.hateoas.config.WebConverters and then it cannot find a match, so it returns null which indicates it cannot write, thus moving onto the other MappingJackson2HttpMessageConverter.

	private ObjectMapper selectObjectMapper(Class<?> targetType, @Nullable MediaType targetMediaType) {
		if (targetMediaType == null || CollectionUtils.isEmpty(this.objectMapperRegistrations)) {
			return this.defaultObjectMapper;
		}
		for (Map.Entry<Class<?>, Map<MediaType, ObjectMapper>> typeEntry : getObjectMapperRegistrations().entrySet()) {
			if (typeEntry.getKey().isAssignableFrom(targetType)) {
				for (Map.Entry<MediaType, ObjectMapper> objectMapperEntry : typeEntry.getValue().entrySet()) {
					if (objectMapperEntry.getKey().includes(targetMediaType)) {
						return objectMapperEntry.getValue();
					}
				}
				// No matching registrations
				return null;
			}
		}
		// No registrations
		return this.defaultObjectMapper;
	}

Is spring hateoas effectively invalidating this converter for anything except RepresentationModel?

kllivecche avatar Jan 31 '22 20:01 kllivecche