本格派エンジニアの工具箱

第28回「Jackson Java JSON-processor」JSONデータの読み書きを行う

ツリーモデルAPIでJSONデータを読み込む

前回は「Jackson Java JSON-processor」⁠以下、Jackson)のストリーミングAPIを利用して、JavaプログラムからJSON形式のデータにアクセスする方法を解説しました。今回はそれに引き続き、ツリーモデルのAPIを使う方法を紹介します。JacksonのツリーモデルAPIは、XMLのDOM APIに相当するもので、一度メモリ上に全てのデータを読み込んでオブジェクトのツリーを構築するため、先頭から順番にデータを読み込むストリーミングAPIに比べて柔軟なデータアクセスが可能です。

ツリーモデルの核になるのは、ブジェクトツリーのノードを表すJsonNodeクラスです。JsonNodeオブジェクトは、複数の子ノードと、それに紐付くフィールド名の情報を保持しています。子ノードを表すJsonNodeオブジェクトはget()メソッドで取得することができます。JsonNodeが配列のノードを表すものである場合には、get()メソッドの引数には取得したいノードのインデックスを整数で指定します。JsonNodeがオブジェクトを表すものである場合にはフィールド名を文字列で指定します。JsonNodeが文字列や数値などの値を持つものである場合には、getXxxxValue()というメソッドでその値を取得できます。文字列の値であればgetStringValue()メソッドです。なお、これらのノードの種類はorg.codehaus.jackson.nodeパッケージ以下のXxxxNodeクラスで区別されます。

// 配列のノードから2番目の子ノードを取得
JsonNode secondNode = node.get(2);

// オブジェクトのノードからフィールド名「name」のノードを取得
JsonNode nameNode = node.get("name");

// ノードから文字列値を取得
String value = node.getStringValue();

ファイルかデータを読み込んでツリーを構築するにはObjectMapperクラスを利用します。まず、ObjectMapperのreadValue()メソッドを用いてルートノードのJsonNodeオブジェクトを取得しましょう。readValue()の第一引数には読込み元のFileオブジェクトや入力ストリームを、第二引数にはJsonNodeのClassオブジェクトを指定します。

// ObjectMapperを作成
ObjectMapper mapper = new ObjectMapper();
// mydata.jsonからルートノードを取得
JsonNode rootNode = mapper.readValue(new File("mydata.json"), JsonNode.class);

今回は、次のようなJSONデータを読み込むケースを考えてみます。

[{
  "name" : { "first" : "太郎", "last" : "技評" }, 
  "mail" : "taro@example.jp"
}, 
{
  "name" : { "first" : "次郎", "last" : "技術" }, 
  "mail" : "jiro@example.jp"
}, 
{
  "name" : { "first" : "花子", "last" : "評論" }, 
  "mail" : "hanako@example.jp"
}]

このデータを読み込んで、その内容を表示するコードの例は次のようになります。

// ObjectMapperを作成
ObjectMapper mapper = new ObjectMapper();
// ルートノードを取得
JsonNode rootNode = mapper.readValue(new File("mydata.json"), JsonNode.class);
      
JsonNode current;
for (int i=0; (current = rootNode.get(i)) != null; i++) {
    // "name"オブジェクトのノードを取得
    JsonNode nameNode = current.get("name");
    System.out.println("name: ");
    // "name"オブジェクトのフィールドデータを取得して表示
    Iterator<String> nameNodeFields = nameNode.getFieldNames();
    while (nameNodeFields.hasNext()) {
	String nameNodeField = nameNodeFields.next();
	System.out.println("    " + nameNodeField + ": " + nameNode.get(nameNodeField));
    }
    
    // "mail"フィールドのノードを取得してデータを表示
    JsonNode mailNode = current.get("mail");
    System.out.println("mail: " + mailNode.getTextValue());
}

この例では、ルートノードと"name"オブジェクトには子ノードがいくつあっても走査できるようになっています。配列ノードに対するget()メソッドは、対象のノードが存在しない場合にはnullを返すので、これを終了条件にしています。オブジェクトノードからは、getFieldNames()メソッドでフィールド名のリストがIteratorオブジェクトとして取得できるため、すべての子ノードを走査する場合に利用できます。

name: 
    first: "太郎"
    last: "技評"
mail: taro@example.jp
name: 
    first: "次郎"
    last: "技術"
mail: jiro@example.jp
name: 
    first: "花子"
    last: "評論"
mail: hanako@example.jp

このコードを実行すると、コンソールには次のように表示されて、データが読み込めていることが確認できます。

ツリーモデルAPIでJSONデータを書き出す

続いて、データの書き出しを行ってみましょう。先ほども書きましたが、ノードの種類は配列やオブジェクト、各型の値などによって、JsonNodeを継承する次のようなクラスで定義されています。このうち、BinaryNode以下は値を表すノードで、全てValueNodeのサブクラスとなっています。

  • ArrayNode
  • ObjectNode
  • BinaryNode
  • BooleanNode
  • NumericNode
  • POJONode
  • TextNode
  • NullNode

ArrayNodeにノードや値を追加するには、add()またはaddXxxx()メソッドを使用します。addXxxx()メソッドは新規にArrayNodeやObjectNodeを作成して追加するためのメソッドで、追加したノードオブジェクトを戻り値として返します。たとえば次のように記述した場合、rootNodeにはObjectNodeが新規に追加され、firstObjectにはそのObjectNodeが返されます。

ObjectNode firstObject = rootNode.addObject();

ObjectNodeにノードや値を追加する場合には、putまたはputXxxx()メソッドを使用します。ArrayNodeの場合と同様に、putXxxx()メソッドでは新規にArrayNodeやObjectNodeを作成して追加するためのメソッドです。ただし、addXxxx()と異なり、引数にはフィールド名を指定する必要があります。たとえば次のように記述した場合、firstObjectには「name」というフィールド名を持ったObjectNodeが新規に追加され、nameObject1にはそのObjectNodeが返されます。

ObjectNode nameObject1 = firstObject.putObject("name");

起点となるノードは、ObjectMapperのcreateArrayNode()メソッドやcreateObjectNode()メソッドを使って生成できます。

// ルートノードを配列として作成する場合
ArrayNode rootNode = mapper.createArrayNode();

// ルートノードを配列として作成する場合
ObjectNode rootNode = mapper.createObjectNode();

作成したJSONデータをファイルへ出力するにはObjectMapperのwrite()メソッドを使います。このメソッドにノードオブジェクトを渡すと、それ以下のノードのデータが指定されたファイルや出力ストリームに書き出されます。

mapper.writeValue(new File("output.json"), rootNode);

以上を踏まえて、次のようなJSONデータを生成するプログラムを考えてみましょう。

[{
  "name":{"first":"太郎","last":"技評"},
  "mail":"taro@example.co.jp"
},{
  "name":{"first":"次郎","last":"技術"},
  "mail":"jiro@example.co.jp"
}]

ネストを含む複数のオブジェクトが配列に格納されている形のデータです。これは次のようなコードで生成できます。

// ObjectMapperを作成
ObjectMapper mapper = new ObjectMapper();
    
// ルートノードを配列として作成
ArrayNode rootNode = mapper.createArrayNode();

// 「技評太郎」のデータを追加
ObjectNode firstObject = rootNode.addObject();
ObjectNode nameObject1 = firstObject.putObject("name");
nameObject1.put("first", "太郎");
nameObject1.put("last", "技評");
firstObject.put("email", "taro@example.jp");

// 「技術次郎」のデータを追加
ObjectNode secondObject = rootNode.addObject();
ObjectNode nameObject2 = firstObject.putObject("name");
nameObject2.put("first", "次郎");
nameObject2.put("last", "技術");
secondObject.put("mail", "jiro@example.jp");

// ファイルへ書き出し
try {
    mapper.writeValue(new File("output.json"), rootNode);
} catch (IOException ex) {
    ex.printStackTrace();
}

Javaオブジェクトとのバインディングもサポート

Jacksonには、ストリーミングAPIとツリーモデルAPIの他に、JavaオブジェクトとのバインディングによってJSONデータの読み書きを行うAPIも用意されています。バインディングAPIは、MapやArrayList、StringといったJavaの既存の型に関連付けられるものと、任意のJavaオブジェクト(Java Beanオブジェクト)に関連付けられるものの2種類があります。

前者の場合、JSONのデータ型とJavaのクラスは次の表のように関連付けられます。

JSONの型Javaのクラス
オブジェクトLinkedHashMap<String,Object>
配列ArrayList<Object>
文字列String
整数Integer, Long, BigInteger
浮動小数点数Double(設定によってBigDecimalを使用することも可)
true/falseBoolean
nullnull

後者の場合、任意のJavaクラスのオブジェクトをJSONデータに変換することが可能です。たとえばUserクラスおよびNameクラスが次のように定義されているとします。

public class User {
    private Name name;
    private String mail;
    ......
}

public class Name {
    private String first;
    private String last;
    ......
}

このとき、UserオブジェクトはObjectMapperクラスを利用してJSONデータとして出力することができます。出力方法はJsonNodeの場合と同様です。

Name name = new Name("太郎", "技評");
User user = new User(name1, "taro@example.co.jp");

mapper.writeValue(new File("output.json"), user);

このように、Jacksonでは複数の方法によるJSONデータの読み書きがサポートされているので、用途に応じて使い分けられるという点が大きなメリットになっています。

おすすめ記事

記事・ニュース一覧