動作確認環境

  • Java 16
  • Jackson 2.12.5

クラスでJSONの型を定義する

JacksonでJSONをJavaのオブジェクトに、あるいはその逆をするには、クラスでJSONの型を定義するのが一般的。

例えば{"level1Str":"v1","level1Obj":{"level2Str":"v2"}}というJSONをJavaで読み書きしたければ、以下のようにクラスを定義する。

@NoArgsConstructor(access = AccessLevel.PRIVATE) // Jackson requires for deserialize
@AllArgsConstructor
@Getter
public class SampleJson {
    private String level1Str;
    private SampleJsonInner level1Obj;

    @NoArgsConstructor(access = AccessLevel.PRIVATE) // Jackson requires for deserialize
    @AllArgsConstructor
    @Getter
    public static class SampleJsonInner {
        private String level2Str;
    }
}

読み書きはそれぞれreadValuewriteValueAsStringを使用する。

    static ObjectMapper objectMapper = new ObjectMapper();

    public static void main(String[] args) {
        useClass();
    }

    private static void useClass() {
        try {
            // from JSON
            var jsonObj = objectMapper.readValue("{\"level1Str\":\"v1\",\"level1Obj\":{\"level2Str\":\"v2\"}}", SampleJson.class);
            System.out.println(jsonObj.getLevel1Str()); // v1
            System.out.println(jsonObj.getLevel1Obj().getLevel2Str()); // v2

            // to JSON
            String json = objectMapper.writeValueAsString(new SampleJson("v1", new SampleJsonInner("v2")));
            System.out.println(json); // {"level1Str":"v1","level1Obj":{"level2Str":"v2"}}
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }

APIのリクエストボディ/レスポンスボディなどはクラスに定義するべきである。特徴としては以下のようなものがあれば、クラスに定義したほうが静的型付けの恩恵を得られるはず。

  • 自分が型を決められる
  • 型が毎回変わらない
  • インターフェースとして型を明確化したい

JSONをクラスにマッピングせずに読み書きするにはJsonNodeが便利

自分の提供するAPIではなく外部のAPIのレスポンスボディの極一部を利用したい場合や、ログを分析しやすくするためにJSON形式を使いたいだけでインターフェースとして型を明確にするメリットがほぼない場合は、JSONをクラスにマッピングしない方が楽に扱える。

読みはreadTreeを使う。書きはobjectNodearrayNode等を使う。どちらもJsonNodeを取得できる。

private static void useJsonNode() {
    // from JSON
    try {
        String otherApiResponseBody = "{\"level1Str\":\"v1\",\"level1Obj\":{\"level2Str\":\"v2\"}}";
        JsonNode node = objectMapper.readTree(otherApiResponseBody);
        // 本題ではないので深入りしないが、アクセス方法(path/get/at)と値の取得方法(~Value/as~)にはバラエティがあり、nullの扱い等が異なるので注意
        // 詳しくは関連記事参照
        System.out.println(node.path("level1Str").textValue()); // v1
        System.out.println(node.get("level1Obj").get("level2Str").asText()); // v2
        System.out.println(node.at("/level1Obj/level2Str").asText()); // v2
    } catch (JsonProcessingException e) {
        e.printStackTrace();
    }

    // to JSON
    var factory = JsonNodeFactory.instance;
    JsonNode node = factory.objectNode()
            .put("level1Str", "v1")
            .set("level1Obj", // 階層を掘りたい場合はputじゃなくてsetとobjectNodeを組み合わせる
                 factory.objectNode()
                         .put("level2Str", "v2")
                         .putPOJO("level2Obj", new Dto("v-x"))); // クラスが定義されていれば、これでも階層が掘れる
    System.out.println(node.toString()); // {"level1Str":"v1","level1Obj":{"level2Str":"v2","level2Obj":{"x":"v-x"}}}
}

// to JSONで登場したDto定義
@AllArgsConstructor
@Getter
public class Dto {
    private String x;
}

関連記事: JavaのJacksonでJSONの値を取得するメソッドのバリエーション(path|get|at), (textValue|asText)