mcpField
U
unit-test-json-serialization

unit-test-json-serialization

github.com/giuseppe-trisciuoglioテスト・品質3653.8k 回インストール7 週間前に更新
リポジトリ

unit-test-json-serialization について

Spring の `@JsonTest` と Jackson を使った JSON のシリアライズ・デシリアライズのテスト方法をまとめたスキルです。Java 開発者向けに、マッピングやカスタムシリアライザー、ネスト構造、日付形式、境界ケースを扱います。

主な機能

  1. 01シリアライズとデシリアライズのテスト
  2. 02JacksonTester による JSON パス検証
  3. 03カスタムマッピングとシリアライザーの確認
  4. 04ネストオブジェクトと日付形式のテスト
  5. 05null、欠落フィールド、多態型の検証

活用シーン

  1. 01DTO の JSON 入出力の検証
  2. 02カスタム Jackson シリアライザーとデシリアライザーのテスト
  3. 03日付形式とフィールド名マッピングの確認
  4. 04null と多態型処理の回帰検出

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill unit-test-json-serialization

インストール後、Claude Code で/unit-test-json-serializationを入力して読み込まれたか確認します。

詳細

@JsonTest を使った JSON シリアライズの単体テスト

概要

Spring の @JsonTest と Jackson を使って、JSON のシリアライズとデシリアライズを単体テストするパターンを紹介します。POJO マッピング、カスタムシリアライザー、フィールド名のマッピング、ネストされたオブジェクト、日時のフォーマット、多態型を扱います。

使用する場面

  • DTO の JSON シリアライズ/デシリアライズをテストする場合
  • カスタム Jackson シリアライザー/デシリアライザーを検証する場合
  • @JsonProperty、@JsonIgnore、フィールド名のマッピングを検証する場合
  • 日時フォーマットの処理(LocalDateTime、Date)をテストする場合
  • null の処理やフィールドの欠落をテストする場合
  • 多態型のデシリアライズをテストする場合

手順

  1. テストクラスに @JsonTest を付ける → JacksonTester の自動構成を有効にします
  2. 対象の型に対する JacksonTester を自動注入する → 型安全な JSON アサーションを利用できます
  3. シリアライズをテストする → json.write(object) を呼び出し、extractingJsonPath* で JSON パスを検証します
  4. デシリアライズをテストする → json.parse(json) または json.parseObject(json) を呼び出し、オブジェクトの状態を検証します
  5. 往復処理を検証する → シリアライズ後にデシリアライズし、同じデータであることを確認します(オブジェクトが適切に比較可能な場合)
  6. エッジケースをテストする → null 値、欠落したフィールド、空のコレクション、不正な JSON を確認します
  7. 検証ポイントを追加する:各アサーションの後に、不正なデータを使った場合にテストが適切な理由で失敗することを確認します

例

Maven の設定
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-json</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>
Gradle の設定
dependencies {
  implementation("org.springframework.boot:spring-boot-starter-json")
  testImplementation("org.springframework.boot:spring-boot-starter-test")
}
基本的なシリアライズとデシリアライズ
@JsonTest
class UserDtoJsonTest {

  @Autowired
  private JacksonTester<UserDto> json;

  @Test
  void shouldSerializeUserToJson() throws Exception {
    UserDto user = new UserDto(1L, "Alice", "[email protected]", 25);
    JsonContent<UserDto> result = json.write(user);

    result
      .extractingJsonPathNumberValue("$.id").isEqualTo(1)
      .extractingJsonPathStringValue("$.name").isEqualTo("Alice")
      .extractingJsonPathStringValue("$.email").isEqualTo("[email protected]")
      .extractingJsonPathNumberValue("$.age").isEqualTo(25);
  }

  @Test
  void shouldDeserializeJsonToUser() throws Exception {
    String json_content = "{\"id\":1,\"name\":\"Alice\",\"email\":\"[email protected]\",\"age\":25}";
    UserDto user = json.parse(json_content).getObject();

    assertThat(user.getId()).isEqualTo(1L);
    assertThat(user.getName()).isEqualTo("Alice");
    assertThat(user.getEmail()).isEqualTo("[email protected]");
    assertThat(user.getAge()).isEqualTo(25);
  }

  @Test
  void shouldHandleNullFields() throws Exception {
    String json_content = "{\"id\":1,\"name\":null,\"email\":\"[email protected]\"}";
    UserDto user = json.parse(json_content).getObject();
    assertThat(user.getName()).isNull();
  }
}
カスタム JSON プロパティ
public class Order {
  @JsonProperty("order_id")
  private Long id;

  @JsonProperty("total_amount")
  private BigDecimal amount;

  @JsonIgnore
  private String internalNote;
}

@JsonTest
class OrderJsonTest {

  @Autowired
  private JacksonTester<Order> json;

  @Test
  void shouldMapJsonPropertyNames() throws Exception {
    String json_content = "{\"order_id\":123,\"total_amount\":99.99}";
    Order order = json.parse(json_content).getObject();
    assertThat(order.getId()).isEqualTo(123L);
    assertThat(order.getAmount()).isEqualByComparingTo(new BigDecimal("99.99"));
  }

  @Test
  void shouldIgnoreJsonIgnoreFields() throws Exception {
    Order order = new Order(123L, new BigDecimal("99.99"));
    order.setInternalNote("Secret");
    assertThat(json.write(order).json).doesNotContain("internalNote");
  }
}
ネストされたオブジェクト
public class Product {
  private Long id;
  private String name;
  private Category category;
  private List<Review> reviews;
}

@JsonTest
class ProductJsonTest {

  @Autowired
  private JacksonTester<Product> json;

  @Test
  void shouldSerializeNestedObjects() throws Exception {
    Product product = new Product(1L, "Laptop", new Category(1L, "Electronics"));
    JsonContent<Product> result = json.write(product);

    result
      .extractingJsonPathNumberValue("$.category.id").isEqualTo(1)
      .extractingJsonPathStringValue("$.category.name").isEqualTo("Electronics");
  }

  @Test
  void shouldDeserializeNestedObjects() throws Exception {
    String json_content = "{\"id\":1,\"name\":\"Laptop\",\"category\":{\"id\":1,\"name\":\"Electronics\"}}";
    Product product = json.parse(json_content).getObject();
    assertThat(product.getCategory().getName()).isEqualTo("Electronics");
  }

  @Test
  void shouldHandleListOfNestedObjects() throws Exception {
    String json_content = "{\"id\":1,\"reviews\":[{\"rating\":5},{\"rating\":4}]}";
    Product product = json.parse(json_content).getObject();
    assertThat(product.getReviews()).hasSize(2);
  }
}
日時のフォーマット
@JsonTest
class DateTimeJsonTest {

  @Autowired
  private JacksonTester<Event> json;

  @Test
  void shouldFormatDateTimeCorrectly() throws Exception {
    LocalDateTime dt = LocalDateTime.of(2024, 1, 15, 10, 30, 0);
    json.write(new Event("Conference", dt))
      .extractingJsonPathStringValue("$.scheduledAt").isEqualTo("2024-01-15T10:30:00");
  }
}
カスタムシリアライザー
public class CustomMoneySerializer extends JsonSerializer<BigDecimal> {
  @Override
  public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
    gen.writeString(value == null ? null : String.format("$%.2f", value));
  }
}

@JsonTest
class CustomSerializerTest {

  @Autowired
  private JacksonTester<Price> json;

  @Test
  void shouldUseCustomSerializer() throws Exception {
    json.write(new Price(new BigDecimal("99.99")))
      .extractingJsonPathStringValue("$.amount").isEqualTo("$99.99");
  }
}
多態型のデシリアライズ
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
  @JsonSubTypes.Type(value = CreditCard.class, name = "credit_card"),
  @JsonSubTypes.Type(value = PayPal.class, name = "paypal")
})
public abstract class PaymentMethod { }

@JsonTest
class PolymorphicJsonTest {

  @Autowired
  private JacksonTester<PaymentMethod> json;

  @Test
  void shouldDeserializeCreditCard() throws Exception {
    String json_content = "{\"type\":\"credit_card\",\"id\":\"card123\"}";
    assertThat(json.parse(json_content).getObject()).isInstanceOf(CreditCard.class);
  }

  @Test
  void shouldDeserializePayPal() throws Exception {
    String json_content = "{\"type\":\"paypal\",\"id\":\"pp123\"}";
    assertThat(json.parse(json_content).getObject()).isInstanceOf(PayPal.class);
  }
}

ベストプラクティス

  • 完全に網羅するため、シリアライズとデシリアライズの両方をテストします
  • JSON 文字列全体を比較するのではなく、JSON パスを個別に検証します
  • null の処理を明示的にテストします。@JsonInclude によって、null フィールドが含まれるか除外されるかが異なる場合があります
  • フィールドを正確にアサートするには、extractingJsonPath* メソッドを使います
  • 往復処理をテストします。オブジェクトをシリアライズし、JSON をデシリアライズして、結果が一致することを確認します
  • 空文字列、空のコレクション、深くネストされた構造など、エッジケースを検証します
  • わかりやすさのため、関連するアサーションを1つのテストにまとめます

制約と警告

  • @JsonTest が読み込むコンテキストは限定的です:JSON 関連の Bean のみが対象です。Spring コンテキスト全体を使う場合は @SpringBootTest を使用します
  • Jackson のバージョン:アノテーションのバージョンが、使用中の Jackson のバージョンと一致していることを確認します
  • 日付形式:デフォルトは ISO-8601 です。カスタムパターンには @JsonFormat を使用します
  • null の処理:シリアライズ時に null を除外するには、@JsonInclude(Include.NON_NULL) を使用します
  • 循環参照:無限ループを防ぐには、@JsonManagedReference/@JsonBackReference を使用します
  • 不変オブジェクト:コンストラクターを使ったデシリアライズには、@JsonCreator と @JsonProperty を使用します
  • 多態型:デシリアライズが機能するには、@JsonTypeInfo がサブタイプを正しく識別する必要があります

デバッグのワークフロー

JSON テストが失敗した場合は、次のワークフローに従ってください。

失敗の症状よくある原因確認方法
JsonPath のアサーションが失敗するフィールド名が一致していない@JsonProperty の表記が JSON キーと一致しているか確認します
null が期待されているのに値が返る@JsonInclude(NON_NULL) が設定されているフィールドまたはクラスのアノテーションを確認します
デシリアライズ結果の型が正しくない@JsonTypeInfo がないJSON に型情報のプロパティを追加するか、サブタイプのマッピングを設定します
日付形式が一致しない形式文字列が正しくない@JsonFormat(pattern=...) が期待される文字列と一致しているか確認します
出力にフィールドがない@JsonIgnore または transient 修飾子フィールドに @JsonIgnore が付いているか、transient キーワードが使われているか確認します
ネストされたオブジェクトが null になる内側の JSON がない、または不正解析した JSON をログに出力し、内側の構造が POJO と一致しているか確認します
JsonParseExceptionJSON 文字列の形式が不正JSON 構文を検証し、エスケープされていない文字がないか確認します

修正後の検証チェックポイント:テストを再実行します。成功した場合は、反対のケースを補完するテストを作成してください(たとえば、null の処理を修正した場合は、回帰を防ぐために null 以外の値のテストを追加します)。

参考資料

コメント