Receiving non-unicode REST POST call on SpringBoot 1.5.10 with Jackson2

254 Views Asked by At

Spring boot with Jackson2 assumes any JSON request will be Unicode and fails to tolerate non ascii characters if the encoding is not Unicode.

I saw that might be different by using GSON instead of Jackson2 but I want to try to stick to Jackson2.

Jackson2 supports any Java supported encoding and SpringBoot supports handling any of those encodings too but when working together they assume Unicode.

SpringBoot will assume all requests are UTF-8 unless you dissactivate that behaviour:

server.servlet.encoding.force-request=false

But then the method org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(Type, Class<?>, HttpInputMessage) doesn't have access to the request but it can access the headers in HttpInputMessage but it doesn't on SpringBoot 1.5.10.

It passes the InputStream to Jackson2 without encoding specification and it assumes it's Unicode.

The solution would be to create a InputStreamReader with the encoding you can find in the headers.

It seem's that's the actual behaviour in the current version of SpringBoot but I wonder if I can override the old one in SpringBoot 1.5.10 somehow.

I can extend the class MappingJackson2HttpMessageConverter but I don't know how to make SpringBoot to use the new converter instead of the default one for Jackson2.

I could mess with the classpath to override the whole AbstractJackson2HttpMessageConverter with a custom version but I wouldn't like that as it might break things if I make a fat-jar or a war and maybe other ways too.

1

There are 1 best solutions below

0
aalku On

I found a way to customize MappingJackson2HttpMessageConverter by registering a new bean so I created this configuration class to register an instance of a subclass with the charset behaviour fixed, creating and using a Reader when the charset is not Unicode.

/**
 * Configuration class to override the default
 * MappingJackson2HttpMessageConverter of SpringBoot 1.5 that fails to use the
 * request charset to parse JSON post bodies.
 */
@Configuration
public class Jackson2CharsetSupportConfig {
    
    private final class MappingJackson2HttpMessageConverterExtension extends MappingJackson2HttpMessageConverter {
        private MappingJackson2HttpMessageConverterExtension(ObjectMapper objectMapper) {
            super(objectMapper);
        }
        @Override
        protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
                throws IOException, HttpMessageNotReadableException {

            JavaType javaType = getJavaType(clazz, null);
            return readJavaType(javaType, inputMessage);
        }

        @Override
        public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
                throws IOException, HttpMessageNotReadableException {

            JavaType javaType = getJavaType(type, contextClass);
            return readJavaType(javaType, inputMessage);
        }

        private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) {
            try {
                MediaType contentType = inputMessage.getHeaders().getContentType();
                Charset charset = getCharset(contentType);
                boolean unicode = charset.name().toUpperCase().startsWith("UTF-");

                if (inputMessage instanceof MappingJacksonInputMessage) {
                    Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
                    if (deserializationView != null) {
                        if (unicode) {
                            return this.objectMapper.readerWithView(deserializationView).forType(javaType).
                                    readValue(inputMessage.getBody());
                        } else {
                            return this.objectMapper.readerWithView(deserializationView).forType(javaType).
                                    readValue(asReader(inputMessage.getBody(), charset));
                        }
                    }
                }
                if (unicode) {
                    return this.objectMapper.readValue(inputMessage.getBody(), javaType);
                } else {
                    return this.objectMapper.readValue(asReader(inputMessage.getBody(), charset), javaType);
                }
            }
            catch (JsonProcessingException ex) {
                throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex);
            }
            catch (IOException ex) {
                throw new HttpMessageNotReadableException("I/O error while reading input message", ex);
            }
        }
        
        protected Charset getCharset(MediaType contentType) {
            if (contentType != null && contentType.getCharset() != null) {
                return contentType.getCharset();
            }
            else {
                return StandardCharsets.UTF_8;
            }
        }

        protected Reader asReader(InputStream is, Charset charset) {
            return new InputStreamReader(is, charset);
        }
        
        
    }

    @Autowired
    private GenericWebApplicationContext webApplicationContext;

    @Bean
    public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
        return new MappingJackson2HttpMessageConverterExtension(Jackson2ObjectMapperBuilder.json().applicationContext(webApplicationContext).build());
    }

}