はじめに

システム連携時は、データの型やフォーマットの違いから、データの変換が必要となる場合があります。

例えば、今回担当したプロジェクトでは、日本国内でのみ利用されるシステムだったため、自システム内では常に日本標準時(JST)を基準とし、タイムゾーンのオフセット(+09:00)を付与せずに日時を管理していました。しかし、他のシステムと連携する際には、日時の正確性を担保するためにオフセットを付与した形式でデータのやり取りを行うことが一般的です。今回のプロジェクトでも、外部システムとの連携時にはオフセットを付与した日時形式が求められました。このような場合、システム間で日時データをやり取りするためには、オフセットの付与や除去といった変換処理が必要となります。

本記事ではこの事例をもとに、日時のオフセットの付与と除去の方法を実装例を交えながら解説します。

オフセットとは

オフセットとは、「ある基準からのずれ」を示す値です。

日時と組み合わせることで、主に世界標準時(UTC)とのずれを表し、その時刻がどのタイムゾーンに属するのかを明確にできます。

例えば「+09:00」は、UTCより9時間進んだタイムゾーンであることを示しています。

  • オフセット付き日時例:2025-05-15T12:30:15+09:00
  • オフセットなし日時例:2025-05-15T12:30:15

(いずれもISO 8601の拡張形式)

※ISO 8601形式は、ISOによって国際的に定められた日時の記述方式であり、拡張形式では「YYYY-MM-DDTHH:mm:ss.sss+zz:zz」のようにハイフンや区切り記号を用いて表記します(ミリ秒部分やオフセット部分は任意です)。本記事では日時をISO 8601の拡張形式で記載します。

本記事の前提

本記事で取り上げるプロジェクトでは、以下のような方針を採用しました。

  • 外部システムとの通信時
    オフセット付きの日時文字列でデータを送受信する
  • 自システムの内部
    日本標準時(JST、タイムゾーンID: Asia/Tokyo)を基準とし、タイムゾーン情報を持たないLocalDateTime型で日時を管理する

本記事の実装例もこの方針に準じており、システム内部での日時は日本標準時(JST)を基準としています。

環境

環境は以下の通りです。

  • Java 17
  • Spring Framework 6.1
  • Jackson 2.15

日時データ変換の概要

システム連携時の日時データの変換には、Jacksonというライブラリを利用しました。

Jacksonは、Javaで広く利用されているJSON処理ライブラリであり、JavaオブジェクトとJSONデータの相互変換(シリアライズ/デシリアライズ)を簡単かつ柔軟に行えるのが特徴です。

※シリアライズとは、JSON処理においてJavaオブジェクトをJSONに変換すること、デシリアライズとは、JSONデータをJavaオブジェクトへ変換することです。

 

送受信時の日時データの変換を実現するため、以下のようなデシリアライザとシリアライザを作成しました。

  • 外部システムから受け取ったオフセット付きの日時文字列を含むJSONのリクエストを、LocalDateTime型のJavaオブジェクトに変換するデシリアライザ
  • LocalDateTime型のJavaオブジェクトをオフセット付きの日時文字列に変換し、JSON形式のレスポンスとして返却するためのシリアライザ

大まかな流れは以下の図を参照してください。

以降、Spring FrameworkとJacksonを利用して日時データの変換を行う方法を解説します。

実装

概要

本記事では、日時データの入出力変換を以下の3段階で実装します。

  1. デシリアライザの作成
  2. シリアライザの作成
  3. JacksonのObjectMapperへの登録

※ObjectMapperとは、JavaオブジェクトとJSONの相互変換を簡単に行うことができるクラスです。自作のシリアライザやデシリアライザを登録することで、標準とは異なる変換ルールを設定できます。

1. デシリアライザの作成

外部システムから受信したオフセット付き日時文字列を、内部処理用のLocalDateTime型へ変換するデシリアライザを実装します。

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

/**
 * オフセット付き日時文字列をAsia/Tokyo(日本標準時)のLocalDateTime型の値に変換するデシリアライザ。
 */
public class ZonedToJstLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {

    @Override
    public LocalDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException {

        if (parser.getText().isEmpty()) {
            return null;
        }
        ZonedDateTime parsed = ZonedDateTime.parse(parser.getText());
        return parsed.withZoneSameInstant(ZoneId.of("Asia/Tokyo")).toLocalDateTime();
    }
}

2. シリアライザの作成

内部で保持しているLocalDateTime型を、外部システムへ返却する際にオフセット付き日時文字列に変換するシリアライザを実装します。

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

/**
 * Asia/Tokyo(日本標準時)のLocalDateTime型の値をオフセット付き日時文字列に変換するシリアライザ。
 */
public class JstLocalDateTimeToZonedSerializer extends JsonSerializer<LocalDateTime> {

    @Override
    public void serialize(LocalDateTime value, JsonGenerator g, SerializerProvider provider) throws IOException {

        if (value == null) {
            return;
        }
        ZonedDateTime zoned = value.atZone(ZoneId.of("Asia/Tokyo"));
        g.writeString(zoned.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));

    }
}

3. JacksonのObjectMapperへの登録

上記のシリアライザ/デシリアライザをJacksonのObjectMapperに登録し、アプリケーション全体で有効化します。

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

import java.time.LocalDateTime;

/**
 * Mapper用のConfigクラス。
 */
@Configuration
public class MapperConfig {

    // ObjectMapper生成用ビルダー
    @Autowired
    private Jackson2ObjectMapperBuilder omBuilder;

    /**
     * ObjectMapperを生成する。
     * LocalDateTime用のシリアライザ/デシリアライザを設定する。
     *
     * @return ObjectMapper
     */
    @Bean
    public ObjectMapper createObjectMapper() {
        // ObjectMapperにシリアライザ/デシリアライザを登録
        ObjectMapper om = omBuilder
                // LocalDateTime → オフセット付き日時 変換用シリアライザ
                .serializerByType(LocalDateTime.class, new JstLocalDateTimeToZonedSerializer())
                // オフセット付き日時 → LocalDateTime 変換用デシリアライザ
                .deserializerByType(LocalDateTime.class, new ZonedToJstLocalDateTimeDeserializer())
                .build();
        return om;
    }
}

動作確認

1.SampleRequest、SampleResponse、SampleControllerの作成

上記のシリアライザ、デシリアライザ、ObjectMapperに加え、動作確認のために以下のようなSampleRequest、SampleResponse、SampleControllerを作成します。

import java.time.LocalDateTime;

public class SampleRequest {
    private LocalDateTime requestDateTime;

    public LocalDateTime getRequestDateTime() {
        return requestDateTime;
    }

    public void setRequestDateTime(LocalDateTime requestDateTime) {
        this.requestDateTime = requestDateTime;
    }

    @Override
    public String toString() {
        return "SampleRequest{" +
                "requestDateTime=" + requestDateTime +
                '}';
    }
}
import java.time.LocalDateTime;

public class SampleResponse {
    private LocalDateTime responseDateTime;

    public LocalDateTime getResponseDateTime() {
        return responseDateTime;
    }

    public void setResponseDateTime(LocalDateTime responseDateTime) {
        this.responseDateTime = responseDateTime;
    }
}
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/sample")
public class SampleController {

    @PostMapping("/datetime")
    public SampleResponse echo(@RequestBody SampleRequest request) {
        System.out.println("受信したリクエスト: " + request);
        
        // RequestDateTimeをResponseDateTimeに入れて返却
        SampleResponse response = new SampleResponse();
        response.setResponseDateTime(request.getRequestDateTime());
        return response;
    }
}

2.動作確認方法

動作確認のため、以下のリクエストを送ります。

  • エンドポイント:POST /sample/datetime
  • リクエストボディ
{
    "requestDateTime": "2025-05-15T12:30:15+01:00"
}

期待される動作は以下の3点です。

  • デシリアライザによって、リクエストボディ内の”2025-05-15T12:30:15+01:00″というオフセット付き日時文字列が、JST基準のLocalDateTime型の値(2025-05-15T20:30:15)としてJavaオブジェクトに変換されること
  • コントローラでリクエストをログ出力した際に、LocalDateTime型になっていること
  • シリアライザによって、LocalDateTime型の値が再度オフセット付き日時文字列に変換され、レスポンスのJSONとして返却されること

3.実行結果

  • サーバーログ(LocalDateTime型で扱っている)
受信したリクエスト: SampleRequest(requestDateTime=2025-05-15T20:30:15)
  • レスポンスボディ(オフセットを付けて返却している)
{
    "responseDateTime": "2025-05-15T20:30:15+09:00"
}

この結果から

  • クライアントからサーバーへのリクエスト時に、オフセット付き日時文字列がLocalDateTime型の値に変換されていること
  • サーバーからクライアントへのレスポンス時にも、LocalDateTime型の値がオフセット付き日時文字列に変換できていること

が分かり、シリアライザ/デシリアライザおよびObjectMapperの設定が正しく機能していることが確認できます。

おわりに

本記事では、オフセット付き日時文字列とLocalDateTime型の相互変換方法と、そのためのシリアライザ/デシリアライザの実装と設定方法を紹介しました。

外部システム連携時の日時処理やタイムゾーン変換でお困りの方の参考になれば幸いです。