1. 概要

JSONを使用する場合の一般的な使用例は、あるモデルから別のモデルへの変換を実行することです。 たとえば、複雑で密にネストされたオブジェクトグラフを解析して、別のドメインで使用するためのより単純なモデルにしたい場合があります。

In this quick tutorial, we’ll look at how to map nested values with Jackson to flatten out a complex data structure. JSONを3つの異なる方法で逆シリアル化します。

  • Using @JsonProperty
  • JsonNodeを使用する
  • カスタムJsonDeserializerを使用する

2. Mavenの依存関係

まず、次の依存関係をpom.xmlに追加しましょう。

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.13.3</version>
</dependency>

jackson-databindの最新バージョンはMavenCentralにあります。

3. JSONソース

Consider the following JSON as the source material for our examples.

構造は工夫されていますが、2レベルの深さでネストされたプロパティが含まれていることに注意してください。

{
    "id": "957c43f2-fa2e-42f9-bf75-6e3d5bb6960a",
    "name": "The Best Product",
    "brand": {
        "id": "9bcd817d-0141-42e6-8f04-e5aaab0980b6",
        "name": "ACME Products",
        "owner": {
            "id": "b21a80b1-0c09-4be3-9ebd-ea3653511c13",
            "name": "Ultimate Corp, Inc."
        }
    }  
}

4. 簡略化されたドメインモデル

In a flattened domain model described by the Product class below, we’ll extract brandName, which is nested one level deep within our source JSON.

Also, we’ll extract ownerName, which is nested two levels deep and within the nested brand object:

public class Product {

    private String id;
    private String name;
    private String brandName;
    private String ownerName;

    // standard getters and setters
}

5. 注釈を使用したマッピング

ネストされたbrandNameプロパティをマップするには、最初にネストされたbrandオブジェクトをMapに解凍し、nameプロパティを抽出する必要があります。 To map ownerName, we unpack the nested owner object to a Map and extract its name property.

@JsonPropertyと、 Product クラスに追加するカスタムロジックの組み合わせを使用して、ネストされたプロパティをアンパックするようにJacksonに指示できます。

public class Product {
    // ...

    @SuppressWarnings("unchecked")
    @JsonProperty("brand")
    private void unpackNested(Map<String,Object> brand) {
        this.brandName = (String)brand.get("name");
        Map<String,String> owner = (Map<String,String>)brand.get("owner");
        this.ownerName = owner.get("name");
    }
}

これで、クライアントコードは ObjectMapper を使用して、テストクラス内にString定数SOURCE_JSONとして存在するソースJSONを変換できます。

@Test
public void whenUsingAnnotations_thenOk() throws IOException {
    Product product = new ObjectMapper()
      .readerFor(Product.class)
      .readValue(SOURCE_JSON);

    assertEquals(product.getName(), "The Best Product");
    assertEquals(product.getBrandName(), "ACME Products");
    assertEquals(product.getOwnerName(), "Ultimate Corp, Inc.");
}

6. JsonNodeを使用したマッピング

Mapping a nested data structure with JsonNode requires a little more work.

Here we use ObjectMapper‘s readTree to parse out the desired fields:

@Test
public void whenUsingJsonNode_thenOk() throws IOException {
    JsonNode productNode = new ObjectMapper().readTree(SOURCE_JSON);

    Product product = new Product();
    product.setId(productNode.get("id").textValue());
    product.setName(productNode.get("name").textValue());
    product.setBrandName(productNode.get("brand")
      .get("name").textValue());
    product.setOwnerName(productNode.get("brand")
      .get("owner").get("name").textValue());

    assertEquals(product.getName(), "The Best Product");
    assertEquals(product.getBrandName(), "ACME Products");
    assertEquals(product.getOwnerName(), "Ultimate Corp, Inc.");
}

7. カスタムJsonDeserializerを使用したマッピング

Mapping a nested data structure with a custom JsonDeserializer is identical to the JsonNode approach from an implementation point of view.

We first create the JsonDeserializer:

public class ProductDeserializer extends StdDeserializer<Product> {

    public ProductDeserializer() {
        this(null);
    }

    public ProductDeserializer(Class<?> vc) {
        super(vc);
    }

    @Override
    public Product deserialize(JsonParser jp, DeserializationContext ctxt) 
      throws IOException, JsonProcessingException {
 
        JsonNode productNode = jp.getCodec().readTree(jp);
        Product product = new Product();
        product.setId(productNode.get("id").textValue());
        product.setName(productNode.get("name").textValue());
        product.setBrandName(productNode.get("brand")
          .get("name").textValue());
        product.setOwnerName(productNode.get("brand").get("owner")
          .get("name").textValue());		
        return product;
    }
}

7.1. デシリアライザーの手動登録

To manually register our custom deserializer, our client code must add the JsonDeserializer to a Module, register the Module with an ObjectMapper and call readValue:

@Test
public void whenUsingDeserializerManuallyRegistered_thenOk()
 throws IOException {
 
    ObjectMapper mapper = new ObjectMapper();
    SimpleModule module = new SimpleModule();
    module.addDeserializer(Product.class, new ProductDeserializer());
    mapper.registerModule(module);

    Product product = mapper.readValue(SOURCE_JSON, Product.class);
 
    assertEquals(product.getName(), "The Best Product");
    assertEquals(product.getBrandName(), "ACME Products");
    assertEquals(product.getOwnerName(), "Ultimate Corp, Inc.");
}

7.2. デシリアライザーの自動登録

As an alternative to the manual registration of the JsonDeserializer, we can register the deserializer directly on the class:

@JsonDeserialize(using = ProductDeserializer.class)
public class Product {
    // ...
}

With this approach, there is no need to register manually.

自動登録を使用してクライアントコードを見てみましょう。

@Test
public void whenUsingDeserializerAutoRegistered_thenOk()
  throws IOException {
 
    ObjectMapper mapper = new ObjectMapper();
    Product product = mapper.readValue(SOURCE_JSON, Product.class);

    assertEquals(product.getName(), "The Best Product");
    assertEquals(product.getBrandName(), "ACME Products");
    assertEquals(product.getOwnerName(), "Ultimate Corp, Inc.");
}

8. 結論

In this article, we demonstrated several ways of using Jackson to parse JSON containing nested values. Have a look at our main Jackson Tutorial page for more examples.

And, as always, code snippets can be found over on GitHub.