[Java][Jackson][怪談] ObjectMapperのFormatが効かないDate

2014.11.21

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

JavaのDate

こんにちは。小室です。
Javaで開発したことがある人にはよく御存知の通り、Javaには昔から日付Date型が二種類、java.sql.Datejava.util.Dateがあります。
まあ、世の中には意外と意識せずに使う人とかも多く、地雷を踏み抜いたり(踏み抜いたまま引き継いだり)した人も多いかと思います。あるある話の一つなのではないでしょうか。
自分も今回久しぶりに地雷を踏み抜いたので、自らの迂闊さへの反省とJavaの厄介なDateクラスへの若干の怒りとともにここに記します。

JacksonのObjectMapperで簡単Jsonオブジェクト変換

JacksonはJavaでの開発ではよく使われるJsonプロセッサの一つです。
データの入れ物T型を定義しておくだけで、簡単にJsonオブジェクトからT型、逆にT型からJsonオブジェクトの変換を行ってくれます。

その中で日付のフォーマットの扱いというのも非常に簡単にできます。
日時の表記というのは、それこそ千差万別、「yyyyMMdd」もあれば「yyyy-MM-dd HH:mm:ss」かもしれないし、はたまたTimezoneつきかもしれない。「yyyy/MM/dd」なんて可能性もありますね。
時と場合と地域によって様々です。

JacksonのObjectMapperは、DateFormatterを指定することができます。
そのため、どんな表記であっても「Date型からJsonオブジェクトの中の日付は指定のフォーマットの文字列へ」相互変換することが簡単にできると思っていたのです。

事故が起きるまでは

動作がおかしい??

「JacksonのObjectMapperを使っていて何の問題が発生したのか?」
それはどんなDateFormatterを設定しても全く変換されないDate型の変数があったのです。その変数は他の定義と同じくjava.util.Dateで指定しているはずなのに。
何故か一つだけ、かたくなにFormatterの指定をスルーし続けました。

public class CompareDate {
  
  /** java.util.Date */
  public Date date1;
  
  /** java.util.Date */
  public Date date2;

  public static CompareDate createDefault() {
    // 現在日時を設定する
    CompareDate result = new CompareDate();
    long currentTimes = System.currentTimeMillis();

    result.date1 = new Date(currentTimes);
    result.date2 = new java.sql.Date(currentTimes);
    return result;
  }
}

まあ、ちょっと小細工が入ってますが、こんな入れ物を用意しておきます。
今回Jsonでは「yyyyMMdd」と変換してもらうようにObjectMapperを設定します。

ObjectMapper mapper = new ObjectMapper();
mapper.setDateFormat(new SimpleDateFormat("yyyyMMdd"));
Json.setObjectMapper(mapper);

これでDateで表現された値は全て指定のFormatに変換されます。

CompareDate compareDate = CompareDate.createDefault();
Json.stringify(Json.toJson(sample);

結果を見てみます。

{"date1":"20141121","date2":"2014-11-21"}

同じjava.util.Dateのはずですが、同じ結果になりません。

犯人はこいつです。

result.date2 = new java.sql.Date(currentTimes);

解決編

今回のイントロで既にネタ晴らしされていますが、Javaの中にある2種類のDate型の罠でした。
一つずつ解説します。

java.sql.Dateとjava.util.Date

java.sql.Dateとjava.util.Dateの変換というのは、かなり昔から話題にされており、すでに昔話の域かもしれません。
厄介なのが、この二つのDateは似て非なるものです。ひがさんの記事を見てみると

時間、分、秒、ミリ秒をゼロに設定することで、「標準化」する必要があります。

これは、すなわち精度が異なるということになります。

java.sql.Dateはjava.util.Dateのサブクラス

java.sql.Dateは、java.util.Dateのサブクラスとして定義されています。
java.sql.Dateで変数が定義されていれば、どんなうかつなプログラマーでも警戒します。
しかし、java.util.Dateのサブクラスであるが故に、java.util.Dateで定義された変数へ代入することも出来てしまうのです。

java.sql.DateはObjectMapperのDateFormatの指定を拒む

今回初めてわかったことなのですが、JacksonのObjectMapperで指定したDateFormatterはjava.util.Dateにしか適用されません。そのサブクラスであるjava.sql.Dateは完全にスルーされます。
厄介なのが、変数として定義してあるのはjava.util.Dateなのに、代入されている値がjava.sql.Dateだと見た目同じに見えてしまう点です。
ObjectMapperを通した後の結果が変わりますが、元の代入元までさかのぼらないと原因が分かりません。

まとめ

今回は代入箇所がすぐ近くだったのですぐ気づきました。これが遠く、さらに自分以外の担当者が入れていたとしたらどうでしょうか?
気づく人は少ないのではないかと思います。

あなたがjava.util.Dateで宣言している値に入ってきてるのは、本当にjava.util.Dateの値ですか?
誰かが勝手にjava.sql.Dateの値を入力してたりしていませんか?

事故が起きてからでは遅いかもしれませんよ?

それでは皆様ごきげんよう

参照