RequestBodyをつけた入力パラメータにはStringTrimmerEditorを含むカスタムPropertyEditorが効かない件

# はじめに
入力バラメータの空文字をNULLとして受け取りたい、とか、Boolean値をtrue,falseに限定したい、とか、[こちらで紹介したような数値を半角に限定したい](https://qiita.com/norippe3/items/0eeb7a39c36ce85afed9)、とか、そういうケースでは、コントローラーでカスタムPropertyEditorをInitBinderにて設定することで対応出来ました。
しかし、RequestBodyアノテーションを付与した場合、JSONでの受け取りということで、jacksonが適用され、コントローラーでバインドしたカスタムPropertyEditorが適用されなくなってしまいます。
そこで、JSONで入力パラメータを受け取る場合は対処法が全く異なってきます。

ネット上でいろいろ探したのですが、Spring MVC だからなのかもしれませんが、WebMvcConfigにObjectMapperをBean定義する方法や、Jackson2ObjectMapperBuilderをBean定義する方法は、デフォルトのものが適用されてしまい定義したBeanは適用されませんでした。

もう一つの方法、``configureMessageConverters``メソッドをオーバーライドする方法は、デフォルトで適用されているObjectMapperの様々な定義情報が綺麗さっぱりなくなってしまい、別な箇所で不具合出たり、とうまくいきませんでした。

そして、ソースを解析したり色々試して、以下の解決法にたどり着きました。

# 解決法
## 空文字をNULLで受け取る
WebMvcConfigを以下のように、``extendMessageConverters``メソッドをオーバーライドします。

```java:WebMvcConfig

// configureMessageConverters だと既に登録されているHttpMessageConverterがクリアされてしまうため、extendMessageConvertersを利用している。
@Override
public void extendMessageConverters(
        List<HttpMessageConverter<?>> converters) {

    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    // 定義済みのMappingJackson2HttpMessageConverterを取得
    for (HttpMessageConverter<?> httpMessageConverter : converters) {
        if (httpMessageConverter instanceof MappingJackson2HttpMessageConverter) {
            converter = (MappingJackson2HttpMessageConverter) httpMessageConverter;
            break;
        }
    }
    
    SimpleModule module = new SimpleModule();

    // 空文字をNULLに変換するデシリアライザーを追加する
    module.addDeserializer(String.class, new StringDeserializer() {
        @Override
        public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
            String result = super.deserialize(p, ctxt);
            if (StringUtils.hasText(result)) {
                return result.trim();
            } else {
                return null;
            }
        }
    });

    converter.getObjectMapper().registerModule(module);
}

```
以上で、文字列は空文字からNULLに変換されてパラメータがマッピングされます。
なお、上記例では、文字列は``trim()``しているので、空白だけの文字列もNULLに変換され、そして、それ以外の文字列はすべてtrimした状態でBeanに設定されます。

## Booleanの値をtrue,falseにする
上記の空文字をNULLで受け取る方法の``module``に、Booleanの値をtrue,falseにするためのデシリアライザーを``SimpleModule#addDeserializer()``で追加するだけです。
それ以降の処理は同じです。

```java:WebMvcConfig
@Override
public void extendMessageConverters(
    List<HttpMessageConverter<?>> converters) {

    // ...
    
    SimpleModule module = new SimpleModule();

    // ...

    // Booleanをtrue,falseに限定するデシリアライザーを追加
    module.addDeserializer(Boolean.class, new StdDeserializer<>(Boolean.class) {
        @Override
        public Boolean deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
            String text = p.getText();
            if (!StringUtils.hasText(text)) {
                return null;
            }
            text = text.trim();
            if ("true".equalsIgnoreCase(text) || "false".equalsIgnoreCase(text)) {
                return Boolean.valueOf(text);
            } else {
                return (Boolean) ctxt.handleWeirdStringValue(_valueClass, text,
                        "only \"true\" or \"false\" recognized");
            }
        }
    });

    converter.getObjectMapper().registerModule(module);
}
```

## IntegerやLong値を半角整数に限定
上記の方法と同じくデシリアライザーを``SimpleModule#addDeserializer()``で追加します。
半角数値をチェックする部分は共通のため、メソッドを外だししています。

```java:WebMvcConfig
@Override
public void extendMessageConverters(
    List<HttpMessageConverter<?>> converters) {

    // ...
    
    SimpleModule module = new SimpleModule();

    // ...

    // Integerを半角数値に限定するデシリアライザーを追加
    module.addDeserializer(Integer.class, new StdDeserializer<>(Integer.class) {
        @Override
        public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
            Object text = isHalfNumber(p, ctxt, _valueClass);
            return text == null ? null : _parseInteger(ctxt, (String) text);
        }
    });
    // Longを半角数値に限定するデシリアライザーを追加
    module.addDeserializer(Long.class, new StdDeserializer<>(Long.class) {
        @Override
        public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
            Object text = isHalfNumber(p, ctxt, _valueClass);
            return text == null ? null : _parseLong(ctxt, (String) text);
        }
    });

    converter.getObjectMapper().registerModule(module);
}

// 半角整数であることをチェックする
private static Object isHalfNumber(JsonParser p, DeserializationContext ctxt, Class<?> _valueClass)
        throws IOException {
    String text = p.getText();
    if (!StringUtils.hasText(text)) {
        return null;
    }
    text = text.trim();
    if (!text.matches("^(\\+|\\-)?[0-9]+$")) {
        return ctxt.handleWeirdStringValue(_valueClass, text,
                "半角数値以外を含む");
    }
    return text;
}
```

# まとめ
``extendMessageConverters()``メソッドをオーバーライドし、既に定義済みの``MappingJackson2HttpMessageConverter``を取得して、``MappingJackson2HttpMessageConverter``の``ObjectMapper``に、deserializerを対応する型ごとに登録した``SimpleModule``を登録する、という流れになります。

もちろん、ObjectMapperには同時にserializerの登録も出来ますので、例えば、JSON出力時に空文字をNULLとして出力したい、とか、Date型の出力形式を統一したい、というケースにも適用出来るかと思います。

# 参考サイト

https://qiita.com/tsuyoz/items/4bafa10a0084667f9a8a

https://www.greptips.com/posts/1357/

https://qiita.com/dayflower/items/67503dbe3157d13db547

https://terasolunaorg.github.io/guideline/current/ja/Tutorial/TutorialREST.html#spring-mvc-rest