Redisson/Jackson による LocalDateTime のデフォルト直列化挙動と書き込み・読み出し非対称の境界
技術ノート
公開 / DavidHLPL
結論を先に:Redisson 3.50.0 / Spring Data Redis 3.5.13 という固定バージョンの境界内では、デフォルトの Jackson 設定は LocalDateTime に対して書き込み時に失敗する(エンコード側が InvalidDefinitionException を投げる)のであって、「書き込み成功・読み出し失敗」ではない。真の書き込み・読み出し非対称は、書き込み側と読み出し側の Jackson 設定が一致しないときだけ現れる。
適用バージョンとシナリオ
| 項目 | バージョン境界 |
|---|---|
| Redisson | 3.50.0(git tag redisson-3.50.0、commit f192ec15) |
| Jackson | jackson-databind / jackson-datatype-jsr310 2.18.2(redisson-parent 3.50.0 の BOM) |
| Spring Data Redis | 3.5.13 |
| 実験環境 | OpenJDK 21、Maven、純メモリでのエンコード/デコード、Redis server 不要 |
適用シナリオ:Spring Data Redis の RedisTemplate や Redisson で、LocalDateTime/LocalDate/LocalTime フィールドを含む POJO を Jackson 直列化するときに、「直接 not supported by default を投げる」または本番での書き込み・読み出し不一致に遭遇したケース。以下の結論は固定タグのソースと最小実験による。Redisson 2.x、Spring Data Redis 4.x、他の Jackson メジャーバージョンは未検証であり、外挿できない。
根本原因:なぜ「書き込み時に失敗」か
1. デフォルト ObjectMapper は JavaTimeModule を登録しない
JsonJacksonCodec()のデフォルトコンストラクタはnew ObjectMapper()。init()は包含ポリシーとフィールド可視性の調整、FAIL_ON_UNKNOWN_PROPERTIES/FAIL_ON_EMPTY_BEANSの無効化、WRITE_BIGDECIMAL_AS_PLAIN/SORT_PROPERTIES_ALPHABETICALLYの有効化、Throwable mixin の追加だけを行う。initTypeInclusion()はDefaultTyping.NON_FINAL+JsonTypeInfo.Id.CLASSを設定する(出力に@classプロパティを含む)。一連の処理で日付モジュールは一切登録されない。jackson-datatype-jsr310は Redisson 3.50.0 の compile 依存だが、jar が classpath にあること ≠ モジュールの登録だ。new ObjectMapper()は ServiceLoader の自動検出をしない。findAndRegisterModules()または明示的なregisterModule(new JavaTimeModule())が必要。- バージョン抜き取り確認:3.9.0、3.12.5、3.17.7、3.25.0、3.30.0、3.40.0、3.44.0、3.46.0、3.50.0 の
JsonJacksonCodecコンストラクタはすべてnew ObjectMapper()で挙動も一致。2.x は未確認。 GenericJackson2JsonRedisSerializer(3.5.13)のデフォルトコンストラクタチェーンもnew ObjectMapper()+ NullValueSerializer モジュール + default typing(Id.CLASS)であり、JavaTimeModule は同様に未登録。
したがって、LocalDateTime フィールドを含む POJO では、デフォルト設定で encode/serialize 時に次が投げられる:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Java 8 date/time type `java.time.LocalDateTime` not supported by default:
add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling
失敗は書き込み側で起こり、「書き込み成功・読み出し失敗」には到達しない。plain な new ObjectMapper() は LocalDateTime に対して読み書き両側で同じ例外を投げる。
2. Redisson のデフォルト codec は JsonJacksonCodec ではない
Config は getCodec() == null のとき new Kryo5Codec() にフォールバックする。JsonJacksonCodec は明示的に config.setCodec(...) しないと効かない。デフォルト経路では LocalDateTime の直列化挙動は Kryo5 が決め、このページの Jackson 分析とは無関係だ。
最小の修正設定
JsonJacksonCodec
ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());
config.setCodec(new JsonJacksonCodec(mapper));
GenericJackson2JsonRedisSerializer
ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());
// 既存データが "@class" に依存している場合は同じ属性名を維持すること。本番ではデシリアライズ可能な型を制限する。
mapper.activateDefaultTypingAsProperty(
mapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL,
"@class");
RedisSerializer<Object> serializer = new GenericJackson2JsonRedisSerializer(mapper);
書き込み・読み出しの両側(異なる RedisTemplate インスタンス、key と value serializer)は同じ設定を使わなければならない。次節参照。
配列形式 vs ISO 形式
- デフォルトは
WRITE_DATES_AS_TIMESTAMPS=true(ObjectMapper のデフォルト)→LocalDateTimeは配列[年,月,日,時,分,秒]に直列化される。 - この feature を無効化すると ISO 文字列、例:
"2026-08-13T10:30:45"。 - jsr310 のデシリアライザは配列と ISO 文字列の両方の入力を受け付ける。同じ codec 内の round-trip は等価で、保存形式自体は読み出し失敗を引き起こさない。
- ただし
WRITE_DATES_AS_TIMESTAMPSは出力形式に影響する設定項目なので、書き込み側と読み出し側を揃え、暗黙の差異に依存しないようにする。
default typing とモジュールの一貫性
- default typing は
@class型メタデータに依存する。default typing のない普通の ObjectMapper で書いた JSON を、default typing 付きのGenericJackson2JsonRedisSerializerで読むと、SerializationException: Could not read JSON ... need JSON String that contains type idが投げられる。 - モジュール集合(JavaTimeModule を含むか)と default typing は独立した二つの次元で、読み書き両側でそれぞれ一致させなければならない。
トラブルシューティングの判断経路(三層)
次の順で「書き込み例外」または「書き込み成功・読み出し失敗」を特定する:
- Jackson モジュール層:エラーテキストは
Java 8 date/time type ... not supported by default: add Module ...jsr310に固定される。これが出たら jsr310 が欠落または未登録。まず書き込み側と読み出し側それぞれの ObjectMapper がregisterModule(new JavaTimeModule())していることを確認する。実験 F1:jsr310 mapper が書いたバイトを plain ObjectMapper で読む → 書き込みは成功、読み出しはInvalidDefinitionException。「書き込み成功≠読み出し成功」の第一の由来。 - RedisTemplate serializer 層:読み書き両側で異なる
RedisSerializerを設定できる(異なる RedisTemplate インスタンス、key/hash serializer の違い、ロールバック後の設定不一致)。デフォルトのGenericJackson2JsonRedisSerializerは書き込み時に失敗する。両側ともJavaTimeModule付きで default typing が一致したカスタム mapper を設定して初めて読み書き両成功になる。実験 F2:default typing なしで書き込み + デフォルト serializer で読み出し → タイプ id 欠落の例外。時間とは独立した第二の由来。 - Redisson codec 層:同一の
JsonJacksonCodecインスタンスはエンコード/デコードで同じ ObjectMapper を共有する(構築時にcopy())。天然に対称。非対称は、構築時に異なる ObjectMapper を渡したか、同一の key 空間を異なる codec/serializer で混用した場合だけ起こり得る。デフォルト codec は書き込み時に失敗するので、書き込み・読み出し非対称は存在しない。
「書き込み成功≠読み出し成功」の厳密条件:書き込み側と読み出し側の Jackson 設定が一致せず(モジュール集合、WRITE_DATES_AS_TIMESTAMPS、default typing、@JsonFormat など)、その差異が読み出し側で顕在化する場合に限り成立する。LocalDateTime の保存形式(配列 vs ISO)自体は読み出し失敗を引き起こさない。
失敗の境界
- 実 Redis server に未接続:検証したのは codec/serializer のローカルなエンコード/デコード挙動で、ネットワーク往復は未検証。
CborJacksonCodec、MsgPackJacksonCodec、TypedJsonJacksonCodec、JacksonCodecなど他の codec は未検証。- モジュール/typing 不一致以外の書き込み・読み出し非対称の成因(
@JsonFormatパターン不一致、アプリのアップグレード・ロールバック、複数 key 空間の混用)は raw で論理推論と明記され、一つずつ実験されていない。引用時は推論として扱う。 - ロールバック/クリーンアップ:本番で読み出し失敗が出たら、まず書き込み側と読み出し側の設定が同一出典か(同じ codec/serializer 設定クラスか)を確認する。ロールバックは両側同時に行い、読み出し側だけ戻して新たな非対称を作らないようにする。
最小検証
- Redis server 不要:codec/serializer のエンコード/デコードは純メモリ操作。使い捨て Maven プロジェクトが
org.redisson:redisson:3.50.0、jackson-databind:2.18.2、jackson-datatype-jsr310:2.18.2、spring-data-redis:3.5.13に依存し、メインクラスが 8 グループを assert する:plain mapper の書き込み/読み出し失敗、jsr310 の配列/ISO 二形式 round-trip、デフォルト codec の失敗と時間なし POJO round-trip、明示モジュール codec の成功、デフォルト Spring serializer の失敗、設定横断の読み出し失敗。RESULT: ALL PASSを出力する。 - 純ソース再現:固定タグで
JsonJacksonCodecコンストラクタ行を読むと、デフォルトの plain ObjectMapper が確認できる。
証拠と不確実性
- 情報源の事実:
redis-jackson-java-time-contract(固定タグソース + 最小実験)が、デフォルトコンストラクタ、失敗の例外テキスト、Kryo5Codec フォールバック、モジュール登録後の配列/ISO 二形式 round-trip、F1/F2 の二種の設定横断読み出し失敗を裏付ける。 - 本ページの総合:問題を「書き込み時に失敗」と「書き込み・読み出し非対称」の二現象に整理し、三層の判断経路と厳密条件を示す。
- 未確認:実 Redis のネットワーク往復、Redisson の他の codec、Spring Data Redis 4.x と Redisson 2.x の挙動、および
@JsonFormat/ロールバック/複数 key 空間による読み出し失敗は未実験の推論であり、対象バージョンでの実測が必要。
関連ページ
出典と関連文書
研究記録
- 元の文書: src/content/note/ja/redis-jackson-java-time.md
- 出典リビジョン: f49953fb0d4bd52a39bf81167b0c4b7dd7d78a64
- 証拠の状態: 有効