意外と知られていない (かもしれない) jackson ObjectMapper の制限

# はじめに
これは [Java Advent Calendar 2024](https://qiita.com/advent-calendar/2024/java) 4 日目の記事です。

Java 言語自体にフォーカスした内容では無いので、どちらかというと spring-framework のカレンダーとかにぶら下げようと思ったんですが、今年は無かったので :sweat_smile: 

# 環境
- java: 17
- jackson-core: 2.18.2
- spring-boot: 3.3.5
- spring-framework: 6.1.14

# 発端
ログを見たら、ある日見慣れないエラーが出ていました。

```
com.fasterxml.jackson.databind.JsonMappingException:
String value length (XXXXXXXXX) exceeds the maximum allowed (20000000, from `StreamReadConstraints.getMaxStringLength()`) (through reference chain: XXXXXXXXXXXX)
```

なんだこりゃ？
max 20000000 なんていう制限、設定した覚え無いんですケド...

# 調査結果
調べてみると、jackson 自体にデフォルトでいろいろな制限が組み込まれていました。

[StreamReadConstraints.java](https://github.com/FasterXML/jackson-core/blob/jackson-core-2.18.2/src/main/java/com/fasterxml/jackson/core/StreamReadConstraints.java) を見ればわかりますが、以下の制限が入っています。

| 制限 | 設定値 | デフォルト |
| --- | --- | --- |
| 最大の JSON の入れ子レベル | maxNestingDepth | 1,000 |
| JSON 全体のサイズ | maxDocumentLength | (無制限) |
| JSON の構成要素<br>(数字、文字列、 `{` `}` `[` `]` などの記号) | maxTokenCount | (無制限) |
| 数字の桁数 | maxNumberLength | 1,000 |
| 文字列の長さ | maxStringLength | 20,000,000 |
| プロパティ名の長さ | maxPropertyNameLength | 50,000 |

:::note info
※ jackson は jackson-databind-* という兄弟製品で CSV や XML などにも
対応しているので、状況に応じてこれらの設定値の意味合いは変わるようです。
:::

シリアライズ・デシリアライズ処理はそれなりに負荷がかかる処理なので、制限を設けてリソースを過剰に消費しないように守ってくれてたんですね。

# 再現させてみる
今回発生したエラーは `maxStringLength` の制限に抵触していたようなので、再現させてみました。

## 再現コード
```java
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;

public class Hoge {
  public static void main(String... args) throws Exception {
    ObjectMapper objectMapper = new ObjectMapper();
    String json = "{\"value\":\"" + StringUtils.repeat('A', 20_000_001) + "\"}";
    Data data = objectMapper.readValue(json, Data.class);
    System.out.println(data);
  }
}

@lombok.Data
class Data {
  private String value;
}
```

## 実行結果
出、出〜〜〜ッッッ
```
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: String value length (20000001) exceeds the maximum allowed (20000000, from `StreamReadConstraints.getMaxStringLength()`) (through reference chain: Data["value"])
	at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:402)
	at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:361)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.wrapAndThrow(BeanDeserializerBase.java:1964)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:312)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:177)
	at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4917)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3860)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3828)
	at Hoge.main(Hoge.java:8)
Caused by: com.fasterxml.jackson.core.exc.StreamConstraintsException: String value length (20000001) exceeds the maximum allowed (20000000, from `StreamReadConstraints.getMaxStringLength()`)
	at com.fasterxml.jackson.core.StreamReadConstraints._constructException(StreamReadConstraints.java:654)
Caused by: com.fasterxml.jackson.core.exc.StreamConstraintsException: String value length (20000001) exceeds the maximum allowed (20000000, from `StreamReadConstraints.getMaxStringLength()`)

	at com.fasterxml.jackson.core.StreamReadConstraints.validateStringLength(StreamReadConstraints.java:589)
	at com.fasterxml.jackson.core.util.ReadConstrainedTextBuffer.validateStringLength(ReadConstrainedTextBuffer.java:27)
	at com.fasterxml.jackson.core.util.TextBuffer.contentsAsString(TextBuffer.java:491)
	at com.fasterxml.jackson.core.json.ReaderBasedJsonParser.getText(ReaderBasedJsonParser.java:297)
	at com.fasterxml.jackson.databind.deser.std.StringDeserializer.deserialize(StringDeserializer.java:42)
	at com.fasterxml.jackson.databind.deser.std.StringDeserializer.deserialize(StringDeserializer.java:11)
	at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:310)
	... 6 more
```

# 対策 (設定変更)
## Spring を使っている場合
[Jackson2ObjectMapperBuilderCustomizer](https://spring.pleiades.io/spring-boot/api/java/org/springframework/boot/autoconfigure/jackson/Jackson2ObjectMapperBuilderCustomizer.html) があるので、ここで設定をカスタマイズするのが妥当でしょう。

```java
@Configuration
class JacksonConfig {
  @Bean
  Jackson2ObjectMapperBuilderCustomizer customStreamReadConstraints() {
    return (builder) -> builder.postConfigurer((objectMapper) -> objectMapper.getFactory()
        .setStreamReadConstraints(StreamReadConstraints.builder().maxStringLength(100_000_000).build()));
  }
}
```
## システムグローバルで変更する場合
jackson を素で使っている場合は、ObjectMapper のコンストラクタで JsonFactory を指定できるので、そこで設定を変更すればよいでしょう。
あるいは以下のように static メソッドを呼んで設定する方法も手っ取り早いです。
ただし、これは JVM グローバルのデフォルト値を変更するので、すべての ObjectMapper インスタンスに影響が出ます。
```java
// ObjectMapper をインスタンス化する前に実行
StreamReadConstraints.overrideDefaultStreamReadConstraints(
    StreamReadConstraints.builder().maxStringLength(100_000_000).build());
```

# 感想
jackson は Spring MVC の REST API で、リクエスト・レスポンスのシリアライズ・デシリアライズでも使われています。

Spring で Tomcat を使うとき、リクエストが巨大になりがちだということが事前にわかっていれば [server.tomcat.max-http-form-post-size](https://spring.pleiades.io/spring-boot/appendix/application-properties/#application-properties.server.server.tomcat.max-http-form-post-size) などを設定して準備万端と高をくくってましたが、まさか ObjectMapper にこんな制限があったとは。知りませんでした。

そもそも、そんな巨大な情報を JSON で扱うなよ！別のフォーマット選べよ！という意見もありますが... :sweat:

# 参考
- https://github.com/FasterXML/jackson-core/issues/962
- https://github.com/spring-projects/spring-boot/issues/34709