Java で JSON と YAML を変換する (Jackson と SnakeYAML を使って)

## 概要

- Java で JSON と YAML を変換する (Jackson と SnakeYAML を使って)
- Jackson で JSON を Java オブジェクトに変換し、SnakeYAML で Java オブジェクトを YAML に変換する
- SnakeYAML で YAML を Java オブジェクトに変換し、Jackson で Java オブジェクトを JSON に変換する
- 環境: Java 15 (AdoptOpenJDK 15+36) + Jackson Databind 2.11.3 + ShakeYAML 1.27 + Gradle 6.6.1

## Gradle によるビルド環境・実行環境を構築

以下の build.gradle を用意する。

```groovy
plugins {
  id 'java'
}

repositories {
  mavenCentral()
}

dependencies {
  // Jackson Databind を使う
  implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.3'

  // ShakeYAML を使う
  implementation 'org.yaml:snakeyaml:1.27'
}

// JsonToYaml クラスを実行するタスクを定義
task jsonToYaml(type: JavaExec) {
  main = 'JsonToYaml'
  classpath = sourceSets.main.runtimeClasspath
}

// YamlToJson クラスを実行するタスクを定義
task yamlToJson(type: JavaExec) {
  main = 'YamlToJson'
  classpath = sourceSets.main.runtimeClasspath
}
```

## Java オブジェクトの中身を出力するクラスを用意

JSON や YAML を Java オブジェクトに変換した際のデータ構造やクラスについて把握するため、オブジェクトの情報を出力するクラスを用意する。

```java
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * JSON や YAML 由来の Java オブジェクトの中身を出力する。
 */
public class ObjectDumper {

  private static final int INDENT = 2;

  public static String dump(Map<String, Object> obj) {
    List<String> buffer = new ArrayList<String>();
    dumpMap(obj, 0, buffer);
    return String.join("\n", buffer);
  }

  private static void dumpMap(Map<String, Object> obj, int depth, List<String> buffer) {
    for (Map.Entry<String, Object> entry : obj.entrySet()) {
      Object value = entry.getValue();
      if (value instanceof List) {
        buffer.add(line(entry.getKey(), value, depth));
        dumpList((List<Object>) value, depth + 1, buffer);
      } else if (value instanceof Map) {
        buffer.add(line(entry.getKey(), value, depth));
        dumpMap((Map<String, Object>) value, depth + 1, buffer);
      } else {
        buffer.add(line(entry.getKey(), value, depth));
      }
    }
  }

  private static void dumpList(List<Object> array, int depth, List<String> buffer) {
    for (Object element : array) {
      if (element instanceof List) {
        buffer.add(line(element, depth));
        dumpList((List<Object>) element, depth + 1, buffer);
      } else if (element instanceof Map) {
        buffer.add(line(element, depth));
        dumpMap((Map<String, Object>) element, depth + 1, buffer);
      } else {
        buffer.add(line(element, depth));
      }
    }
  }

  private static String line(String key, Object value, int depth) {
    if (value == null) {
      return " ".repeat(INDENT * depth) + key + ": " + "null";
    } else if (value instanceof Map) {
      return " ".repeat(INDENT * depth) + key + ": " + "(" + value.getClass().getName() + ")";
    } else if (value instanceof List) {
      return " ".repeat(INDENT * depth) + key + ": " + "(" + value.getClass().getName() + ")";
    } else {
      return " ".repeat(INDENT * depth) + key + ": " + value.toString() + " (" + value.getClass().getName() + ")";
    }
  }

  private static String line(Object value, int depth) {
    if (value == null) {
      return " ".repeat(INDENT * depth) + "null";
    } else if (value instanceof Map) {
      return " ".repeat(INDENT * depth) + "(" + value.getClass().getName() + ")";
    } else if (value instanceof List) {
      return " ".repeat(INDENT * depth) + "(" + value.getClass().getName() + ")";
    } else {
      return " ".repeat(INDENT * depth) + value.toString() + " (" + value.getClass().getName() + ")";
    }
  }
}
```

## JSON を YAML に変換する

Jackson で変換: JSON → Java オブジェクト
SnakeYAML で変換: Java オブジェクト → YAML

### ソースコード

```java
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;

import java.util.Map;

public class JsonToYaml {

  public static void main(String[] args) throws JsonProcessingException {

    // JSON 文字列
    String json = """
      {
        "key": "value",
        "foo": "bar",
        "hoge": "fuga",
        "datalist": [
          {
            "string": "mojiretu",
            "integer": 123456,
            "decimal": 123.456,
            "boolean": true,
            "date": "2020-01-01",
            "timestamp": "2020-01-01T00:00:00.123456789+09:00",
            "nullonly": null,
            "object": {"hoge": "HOGE", "fuga": "FUGA"},
            "array": ["foo", "bar", "baz", ["spam", "ham", "eggs"]]
          },
          {
            "string": "文字列",
            "integer": 100000,
            "decimal": 999.999,
            "boolean": false,
            "date": "2020-01-01",
            "timestamp": "2020-01-01T00:00:00.123456789+09:00",
            "nullonly": null,
            "object": {"hoge": "ほげ", "fuga": "ふが"},
            "array": ["ふー", "ばー", "ばず", ["スパム", "ハム", "エッグ"]]
          }
        ]
      }
      """;
    System.out.println("********** 入力する JSON 文字列 **********");
    System.out.println(json);

    System.out.println("********** JSON を Java オブジェクト に変換 **********");
    ObjectMapper mapper = new ObjectMapper();
    Map<String, Object> obj = mapper.readValue(json, new TypeReference<Map<String, Object>>() {
    });
    System.out.println(ObjectDumper.dump(obj));

    System.out.println("********** Java オブジェクトを YAML に変換 **********");
    DumperOptions opts = new DumperOptions();
    opts.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
    String yaml = new Yaml(opts).dump(obj);
    System.out.println(yaml);
  }
}
```

### 実行結果

```
********** 入力する JSON 文字列 **********
{
  "key": "value",
  "foo": "bar",
  "hoge": "fuga",
  "datalist": [
    {
      "string": "mojiretu",
      "integer": 123456,
      "decimal": 123.456,
      "boolean": true,
      "date": "2020-01-01",
      "timestamp": "2020-01-01T00:00:00.123456789+09:00",
      "nullonly": null,
      "object": {"hoge": "HOGE", "fuga": "FUGA"},
      "array": ["foo", "bar", "baz", ["spam", "ham", "eggs"]]
    },
    {
      "string": "文字列",
      "integer": 100000,
      "decimal": 999.999,
      "boolean": false,
      "date": "2020-01-01",
      "timestamp": "2020-01-01T00:00:00.123456789+09:00",
      "nullonly": null,
      "object": {"hoge": "ほげ", "fuga": "ふが"},
      "array": ["ふー", "ばー", "ばず", ["スパム", "ハム", "エッグ"]]
    }
  ]
}

********** JSON を Java オブジェクト に変換 **********
key: value (java.lang.String)
foo: bar (java.lang.String)
hoge: fuga (java.lang.String)
datalist: (java.util.ArrayList)
  (java.util.LinkedHashMap)
    string: mojiretu (java.lang.String)
    integer: 123456 (java.lang.Integer)
    decimal: 123.456 (java.lang.Double)
    boolean: true (java.lang.Boolean)
    date: 2020-01-01 (java.lang.String)
    timestamp: 2020-01-01T00:00:00.123456789+09:00 (java.lang.String)
    nullonly: null
    object: (java.util.LinkedHashMap)
      hoge: HOGE (java.lang.String)
      fuga: FUGA (java.lang.String)
    array: (java.util.ArrayList)
      foo (java.lang.String)
      bar (java.lang.String)
      baz (java.lang.String)
      (java.util.ArrayList)
        spam (java.lang.String)
        ham (java.lang.String)
        eggs (java.lang.String)
  (java.util.LinkedHashMap)
    string: 文字列 (java.lang.String)
    integer: 100000 (java.lang.Integer)
    decimal: 999.999 (java.lang.Double)
    boolean: false (java.lang.Boolean)
    date: 2020-01-01 (java.lang.String)
    timestamp: 2020-01-01T00:00:00.123456789+09:00 (java.lang.String)
    nullonly: null
    object: (java.util.LinkedHashMap)
      hoge: ほげ (java.lang.String)
      fuga: ふが (java.lang.String)
    array: (java.util.ArrayList)
      ふー (java.lang.String)
      ばー (java.lang.String)
      ばず (java.lang.String)
      (java.util.ArrayList)
        スパム (java.lang.String)
        ハム (java.lang.String)
        エッグ (java.lang.String)
********** Java オブジェクトを YAML に変換 **********
key: value
foo: bar
hoge: fuga
datalist:
- string: mojiretu
  integer: 123456
  decimal: 123.456
  boolean: true
  date: '2020-01-01'
  timestamp: '2020-01-01T00:00:00.123456789+09:00'
  nullonly: null
  object:
    hoge: HOGE
    fuga: FUGA
  array:
  - foo
  - bar
  - baz
  - - spam
    - ham
    - eggs
- string: 文字列
  integer: 100000
  decimal: 999.999
  boolean: false
  date: '2020-01-01'
  timestamp: '2020-01-01T00:00:00.123456789+09:00'
  nullonly: null
  object:
    hoge: ほげ
    fuga: ふが
  array:
  - ふー
  - ばー
  - ばず
  - - スパム
    - ハム
    - エッグ
```

## YAML を JSON に変換する

SnakeYAML で変換: YAML → Java オブジェクト
Jackson で変換: Java オブジェクト → JSON

### ソースコード

```java
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.yaml.snakeyaml.Yaml;

import java.util.Map;

public class YamlToJson {

  public static void main(String[] args) throws JsonProcessingException {

    // YAML 文字列
    String yaml = """
      key: value
      foo: bar
      hoge: fuga
      datalist:
      - string: mojiretu
        integer: 123456
        decimal: 123.456
        boolean: true
        date: 2020-01-01
        timestamp: 2020-01-01T00:00:00.123456789+09:00
        nullonly: null
        object:
          hoge: HOGE
          fuga: FUGA
        array:
        - foo
        - bar
        - baz
        - - spam
          - ham
          - eggs
      - string: 文字列
        integer: 100000
        decimal: 999.999
        boolean: false
        date: 2020-01-01
        timestamp: 2020-01-01T00:00:00.123456789+09:00
        nullonly: null
        object:
          hoge: ほげ
          fuga: ふが
        array:
        - ふー
        - ばー
        - ばず
        - - スパム
          - ハム
          - エッグ
      """;
    System.out.println("********** 入力する YAML 文字列 **********");
    System.out.println(yaml);

    System.out.println("********** YAML を Java オブジェクト に変換 **********");
    Map<String, Object> obj = new Yaml().load(yaml);
    System.out.println(ObjectDumper.dump(obj));

    System.out.println("********** Java オブジェクトを JSON に変換 **********");
    ObjectMapper mapper = new ObjectMapper();
    String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
    System.out.println(json);
  }
}
```

### 実行結果

```
********** 入力する YAML 文字列 **********
key: value
foo: bar
hoge: fuga
datalist:
- string: mojiretu
  integer: 123456
  decimal: 123.456
  boolean: true
  date: 2020-01-01
  timestamp: 2020-01-01T00:00:00.123456789+09:00
  nullonly: null
  object:
    hoge: HOGE
    fuga: FUGA
  array:
  - foo
  - bar
  - baz
  - - spam
    - ham
    - eggs
- string: 文字列
  integer: 100000
  decimal: 999.999
  boolean: false
  date: 2020-01-01
  timestamp: 2020-01-01T00:00:00.123456789+09:00
  nullonly: null
  object:
    hoge: ほげ
    fuga: ふが
  array:
  - ふー
  - ばー
  - ばず
  - - スパム
    - ハム
    - エッグ

********** YAML を Java オブジェクト に変換 **********
key: value (java.lang.String)
foo: bar (java.lang.String)
hoge: fuga (java.lang.String)
datalist: (java.util.ArrayList)
  (java.util.LinkedHashMap)
    string: mojiretu (java.lang.String)
    integer: 123456 (java.lang.Integer)
    decimal: 123.456 (java.lang.Double)
    boolean: true (java.lang.Boolean)
    date: Wed Jan 01 09:00:00 JST 2020 (java.util.Date)
    timestamp: Wed Jan 01 00:00:00 JST 2020 (java.util.Date)
    nullonly: null
    object: (java.util.LinkedHashMap)
      hoge: HOGE (java.lang.String)
      fuga: FUGA (java.lang.String)
    array: (java.util.ArrayList)
      foo (java.lang.String)
      bar (java.lang.String)
      baz (java.lang.String)
      (java.util.ArrayList)
        spam (java.lang.String)
        ham (java.lang.String)
        eggs (java.lang.String)
  (java.util.LinkedHashMap)
    string: 文字列 (java.lang.String)
    integer: 100000 (java.lang.Integer)
    decimal: 999.999 (java.lang.Double)
    boolean: false (java.lang.Boolean)
    date: Wed Jan 01 09:00:00 JST 2020 (java.util.Date)
    timestamp: Wed Jan 01 00:00:00 JST 2020 (java.util.Date)
    nullonly: null
    object: (java.util.LinkedHashMap)
      hoge: ほげ (java.lang.String)
      fuga: ふが (java.lang.String)
    array: (java.util.ArrayList)
      ふー (java.lang.String)
      ばー (java.lang.String)
      ばず (java.lang.String)
      (java.util.ArrayList)
        スパム (java.lang.String)
        ハム (java.lang.String)
        エッグ (java.lang.String)
********** Java オブジェクトを JSON に変換 **********
{
  "key" : "value",
  "foo" : "bar",
  "hoge" : "fuga",
  "datalist" : [ {
    "string" : "mojiretu",
    "integer" : 123456,
    "decimal" : 123.456,
    "boolean" : true,
    "date" : 1577836800000,
    "timestamp" : 1577804400123,
    "nullonly" : null,
    "object" : {
      "hoge" : "HOGE",
      "fuga" : "FUGA"
    },
    "array" : [ "foo", "bar", "baz", [ "spam", "ham", "eggs" ] ]
  }, {
    "string" : "文字列",
    "integer" : 100000,
    "decimal" : 999.999,
    "boolean" : false,
    "date" : 1577836800000,
    "timestamp" : 1577804400123,
    "nullonly" : null,
    "object" : {
      "hoge" : "ほげ",
      "fuga" : "ふが"
    },
    "array" : [ "ふー", "ばー", "ばず", [ "スパム", "ハム", "エッグ" ] ]
  } ]
}
```

## 参考資料

### JSON + Jackson

- [RFC 8259 \- The JavaScript Object Notation \(JSON\) Data Interchange Format](https://tools.ietf.org/html/rfc8259)
- [GitHub \- FasterXML/jackson: Main Portal page for the Jackson project](https://github.com/FasterXML/jackson)
- [jackson\-core 2\.11\.3 javadoc \(com\.fasterxml\.jackson\.core\)](https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-core/2.11.3/)
- [jackson\-databind 2\.11\.3 javadoc \(com\.fasterxml\.jackson\.core\)](https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/2.11.3/)

### YAML + SnakeYAML

- [YAML Ain’t Markup Language \(YAML™\) Version 1\.1](https://yaml.org/spec/1.1/)
- [Timestamp Language\-Independent Type for YAML™ Version 1\.1](https://yaml.org/type/timestamp.html)
- [asomov / snakeyaml — Bitbucket](https://bitbucket.org/asomov/snakeyaml/src/master/)
- [snakeyaml 1\.27 javadoc \(org\.yaml\)](https://www.javadoc.io/doc/org.yaml/snakeyaml/1.27/)