3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaAdvent Calendar 2024

Day 4

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

Last updated at Posted at 2024-12-03

はじめに

これは Java Advent Calendar 2024 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 を見ればわかりますが、以下の制限が入っています。

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

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

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

再現させてみる

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

再現コード

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 があるので、ここで設定をカスタマイズするのが妥当でしょう。

@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 インスタンスに影響が出ます。

// ObjectMapper をインスタンス化する前に実行
StreamReadConstraints.overrideDefaultStreamReadConstraints(
    StreamReadConstraints.builder().maxStringLength(100_000_000).build());

感想

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

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

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

参考

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?