JavaでYAMLファイルを読み込む(Jackson)

2023年10月20日

JavaでYAML形式のデータを扱うためのライブラリはいくつかありますが、今回はJacksonを使ってみます。
Maven Repositoryでライブラリを検索して、pom.xmlのdependenciesに追加します。
この記事の執筆時の最新バージョンは2.15.2なので、それを使用します。

<properties>
    <maven.compiler.source>18</maven.compiler.source>
    <maven.compiler.target>18</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.15.2</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-yaml</artifactId>
        <version>2.15.2</version>
    </dependency>
</dependencies>

以下のYAMLファイル(users.yaml)を読み込みます。

- email: tanaka@example.com
  name: Tanaka-san
  age: 20
  metadata:
    country: Japan
    city: Tokyo
    message: こんにちは!
- email: smith@example.com
  name: John Smith
  age: 32
  metadata:
    country: United States
    city: New York
    message: Hello!
- email: ivan@example.com
  name: Ivan Petrovich
  age: 51
  metadata:
    country: Russia
    city: Saint-Petersburg
    message: Привет!

YAMLファイルを読み込むJavaコードは以下のような感じです。

package com.github.maeda6uiui.loadyamlsample;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;

class Metadata {
    public String country;
    public String city;
    public String message;

    @Override
    public String toString() {
        return String.format("\tcountry=%s city=%s message=%s", country, city, message);
    }
}

class User {
    public String email;
    public String name;
    public int age;
    public Metadata metadata;

    @Override
    public String toString() {
        var sb = new StringBuilder();

        sb.append(String.format("===== %s =====\n", email));
        sb.append("name=" + name + "\n");
        sb.append("age=" + age + "\n");
        sb.append("metadata=\n");
        sb.append(metadata.toString());

        return sb.toString();
    }
}

public class Main {
    public static void main(String[] args) {
        try {
            String yaml = Files.readString(Paths.get("users.yaml"));

            var mapper = new ObjectMapper(new YAMLFactory());

            //POJOを用いた読込処理
            User[] users = mapper.readValue(yaml, User[].class);
            Arrays.asList(users).forEach(System.out::println);

            //上のArrays.asList...は以下のfor文と同じ処理
            for (int i = 0; i < users.length; i++) {
                System.out.println(users[i]);
            }

            //Mapを用いた読込処理
            var typeRef = new TypeReference<List<Object>>() {
            };
            List<Object> objects = mapper.readValue(yaml, typeRef);
            objects.forEach(System.out::println);

            //JsonNodeを用いた読込処理
            JsonNode node = mapper.readTree(yaml);
            ArrayNode usersNode = (ArrayNode) node;
            for (int i = 0; i < usersNode.size(); i++) {
                JsonNode userNode = usersNode.get(i);
                System.out.println(userNode.get("email").asText());
            }

            //データをJSON形式でファイルに出力する
            var jsonMapper = new ObjectMapper();
            String json = jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(users);
            Files.writeString(Paths.get("users.json"), json);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

POJOを用いてデータを読み込むのが一番基本的な使い方になると思います。
YAMLのデータ構造を表すクラスを用意して、ObjectMapperのreadValueメソッドでデータを読み込みます。

class Metadata {
    public String country;
    public String city;
    public String message;

    @Override
    public String toString() {
        return String.format("\tcountry=%s city=%s message=%s", country, city, message);
    }
}

class User {
    public String email;
    public String name;
    public int age;
    public Metadata metadata;

    @Override
    public String toString() {
        var sb = new StringBuilder();

        sb.append(String.format("===== %s =====\n", email));
        sb.append("name=" + name + "\n");
        sb.append("age=" + age + "\n");
        sb.append("metadata=\n");
        sb.append(metadata.toString());

        return sb.toString();
    }
}

toStringは読み込んだデータをいい感じで表示させるために追加しただけなので、別になくても構いません。

各クラスのフィールド名をYAMLのフィールド名と同じにしておくと、自動的に値がセットされます。
クラスのフィールド名とYAMLのフィールド名が異なる場合は、クラスのフィールドにアノテーションをつけてYAMLのフィールド名を指定します。

@JsonProperty("email")
public String email;

今回の例ではフィールドをpublicにしていますが、privateにする場合はgetterとsetterを追加してください。

一般的には、フィールドはprivateにしておいて、それにアクセスするためのgetterとsetterは別途用意します。
ただ、今回の例のように単純にデータを保存しておくだけのクラスなら、個人的にはフィールドをpublicにしておいた方が便利だし見通しがいいと思います。
(異論は認めます)

Java 16でRecordという仕組みが導入され、データを保存しておくだけのクラスを簡単に記述できるようになりました。
JacksonでもRecordを使えそうですが、手元で検証できていないため、ここでの説明は省略します。

YAMLやJSONに含まれるデータのうち一部だけが必要な場合は、データ用のクラスは用意せずにJsonNodeでデータを解析していくことも可能です。

Jacksonは(たぶん)元々JSONを扱うためのライブラリなので、ほとんど同じコードでJSONを扱うこともできます。
先に紹介したコードの最後では、YAMLファイルから読み込んだデータをJSONファイルに出力する処理を行っています。