Java SEの標準APIだけでJSONを扱うサンプル(JDK 1.6以降、1.8も対応)

 Java SE の標準 API を使って、JSON データを扱い易い Java オブジェクトに変換しようと思ったら、意外と日本語の情報が見当たらなかったので、ごく適当に書いてみます。

 あ、もちろん Jackson や JSONIC、Gson 等の外部ライブラリを使う方法や、Java EE 7 で追加された Java API for JSON Processing(javax.json)についてならば情報は沢山あるんですが、ここで取り上げるのはあくまで Java SE の標準 API だけを使用した方法についてです。

 Java SE 6(JDK 1.6)以降は標準ライブラリで簡単に JavaScript を扱えるので、わざわざ記事にするほど難しいことは無い上に、情報の少なさから言って、多分ほとんど誰も必要としてないんだろうなーとは思うんですが(素直に外部ライブラリを使うか、サーバーサイドなら Java EE 使えばいいだけですもんね)、万が一どなたかの参考になりましたら。

追記:(2014/08/20)
 ちょっと気が早いですが、どうやら Java 9 で Light-Weight JSON API が提供されるらしいですよ(見送られた模様。詳しくは後述)。

 ここで紹介しているような外法に頼らなくても、簡単に JSON を扱えるようになりそうですね。まぁ、いままで提供されていなかったことの方が不思議な訳ですが。

 他にも HTTP 2 Client など、Java 8 の時ほどではないにしろ、気になる新機能が目白押しの予感ですが、なんかどんどん書き方が変わっちゃってめんどくさいなー、過去の資産と入り乱れて分かり難くなってきちゃった感もあり。もういっそのこと、それこそ Light-Weight な別言語にした方がいいんじゃないの。

 追記に追記を重ねた拡張三昧のこの記事も、ホントたいがいだけどね!(笑)

追記:(2015/10/26)
 肥大化の問題については、Java 9 では Module という新機能で解決を模索するようです。

 また書き方増えるのか。面倒臭いなぁ。思惑通りに使われるなら、きっと良いものになるのではないでしょうか。

 ともかく、複雑さが増すだけの結果にならなければ良いなと思います。

追記:(2016/02/08)
 す、すみません、JEP 198: Light-Weight JSON API は、とっくの昔に Java 9 への追加を見送られてしまっていたようなのに、長らく情報を更新できていませんでした。

 Java 10 では追加されるといいですね。さらに書き方増えそうですけど。面倒臭いなぁ。

追記:(2014/12/22)
 追記が遅れてしまいましたが、Java EE 8 でも JSON がより一層サポートされるようです(Java EE 7 で提供された JSON-Processing の拡張と JSON-Binding)。詳しくは、以下の記事等をどうぞ。

 どうでもいいけど、JSONB って、つい最近 PostgreSQL 方面でも話題になってたよね......いや、あっちはバイナリ JSON だけどさ......紛らわしいというか(自粛)、あ、こっちは JSON-B でハイフンが入ってるからいいのか!(納得)

Android API などについて追記:
 そういえば、Android API の org.json パッケージについて全く触れてませんでしたね。

 上記リンク先にあるように、Android 環境でしたら JSON を標準的に扱うことが可能です。ただ、私の中では Java と Androidって割りと別物なので、触れるのをすっかり忘れてました。

 通常の Java SE で同じようなことがしたかったら、全く同じものではありませんが、json.orgでソースコードが公開されていますので、at your own risk で適当に利用したらいいんじゃないかと思います(2016/02/08 追記:と思ったら、いつの間にかソースが消えてる......)。
(パッケージ名が同じなので、多分これ、どこかの時点での json.org が Android API に取り込まれたんですよね? json.org のソースのコピーライトは2002年ですし。Google先生の Android的には、Java の標準 API ではサポートされていなかった JSON を、直ぐにでも標準で扱える必要があったので取り込んだのでしょう。いや、知らないまま適当なこと書いてますけど(笑))

 それから、内容を確認したりリンクのメンテがめんどくさいなーと思って、各ライブラリへはあえてリンクを張ってなかったのですが、ほとんど誰も見ないだろうなぁと思っていたこの記事をご覧になっている方が、何故か予想より相当に多いので、皆様がこんな外法に惑わされないように、この冒頭に真っ当なライブラリへのリンクをまとめておきます。

 いつの間にやらリンクが切れていたら申し訳ありません。あと、リンク先に関して、私は何も責任を負えないので、「お前みたいなモンが張ったリンクなんて、どこに飛ばされるんだか分かったモンじゃない!信用できるか!」という情強な方々は、どうぞご自分の部屋にお戻りになって内側からしっかり鍵をかけて自分で検索して飛んだらいいと思います。残念ながら、現状のネットでは、そういう慎重さって必要ですよね。

 というか、私なんぞがまとめるまでもなく、json.org でもっと詳細にまとまってますね。すみません。

 まー、Java 9 が来るまでは、仕事で使うなら javax.json にしとけばいいんじゃないでしょうか。どこからも文句が出にくいと思いますので。この際、機能は度外視です(笑)

 趣味で使うなら、json.org を自分色に染め直すのが楽しそうでオススメ(2016/02/08 追記:と思ってたんだけど、いつの間にやら json.org からソースが削除されていました。まさか、あそこから消えるとはなぁ......)。

スポンサーリンク

読み込み中です。少々お待ち下さい

サンプルと説明

 では、本題の Java SE 6 ~ 8(JDK 1.6~1.8)までの標準APIだけで、JSON を扱うサンプルコードをば。

注意点
 この記事をご覧になっている方が、何故か予想よりもかなり多いので、下で書いていたセキュリティについての注釈を先頭に移動しておきます。前置きばっかりで、ホントすみません。

 下記サンプルは eval を使っているので、セキュリティ的には決して好ましくありません(ほぼ何でもできてしまいます)。
 要するに、自分で生成した設定ファイル等、出自のはっきりした素性の良いデータとしての JSON をローカルで扱う為の閉じた世界におけるサンプルであり、外部ネットワークから受信した中身の判然としない安全であることが不明のデータを扱うことは想定していません。

 下手に SecurityManager でコントロールするくらいならば、素直に外部ライブラリや javax.json を使うことをお勧めします。

 というよりも、できるからやってみただけという実験的な側面が強く、自分でも外に出すプログラムには、まず使わないです(というか、外部ネットワークと連携することが前提のプログラムで使ってはいけません)。

(2015/10/21追記)
 じゃあ、一体なにに使うねん、という問題に触れていなかったので追記しますが、例えば私はついさっき、HAR(Http ARchive。中身は JSON)ファイルを軽く分析する為に使いました。そんな風に、JSON の中身を調べたいけど、適したアプリが見つからなかったり、素性の分からないアプリを何処かから持ってくるのが憚られるような場合に、自分でちゃっちゃとプログラムを書いて、その場限りで流す時に便利じゃないでしょうか。素の JDK さえあればよく、外部ライブラリも何も必要としないので(しつこいですが、中身を把握していないデータを扱ってはいけません)。

 それから、eval しているだけなので、厳密に言うと JSON の仕様と微妙に挙動が異なる場合があります(JSON の方がいくらかちゃんとしていて、JavaScript はガバガバなので、本来は許可されるべきではない記述が許可されたりします)。

(2014/12/19追記)
 自分の中で大前提過ぎて、すっかり注記するのを忘れていましたが(申し訳ありません)、以下で利用しているクラスは非公開です。JDK の更新により、将来的に動かなくなる可能性があることはご承知おきください。

 それらを踏まえた上で、以下、ご覧ください。

サンプル実行時の引数について:(2014/09/04 追記)
 試しやすいように、第1引数で読み込むファイルのパスを指定できるようにしてみました。
 第1引数がファイルでなかった場合は、引数の文字列をそのまま JSON として扱います。
 引数を何も指定しなかった場合は、これまで通りコード内のサンプルデータを使います。

(2014/11/25追記)
 ファイルの文字コードがシステムの文字コードと異なる場合は、例えば「UTF-8」や「JISAutoDetect」のように第2引数で文字セット名を指定してください。

 このサンプルでのファイル読み込みは、通常の Parser のように JSON データを読み込む訳ではなく、単に eval に渡す文字列を準備するだけの話なので、利用者が多いであろう Windows 環境で無造作にテキストファイルを作成した場合を考慮して、デフォルトを JISAutoDetect としていましたが、やっぱりしっくりこないし、最近は Mac も Linux も UTF-8 だし、Windows でも少し意識すれば普通に UTF-8 を扱えるしなので、システムの文字コードをデフォルトとするように変更しました。
 なので、Windows 環境で UTF-8 のファイルを読み込む場合は、第2引数で「UTF-8」を指定してください。指定したくない場合は SJIS(CP932。というか、システムデフォルト)で保存したファイルを読み込んでください。

 という説明が長ったらしい上にめんどかったので JISAutoDetect をデフォルトにしていたのですが、結局説明を追加してしまったので、単純にシステムデフォルトにしました。仮にも JSON を扱うんだから、本来は UTF-8 がデフォルトでいいんでしょうけど、そうすると上に書いたように Windows がねぇ......という折衷案。

 JsonSample.java

  1. import javax.script.ScriptEngine;
  2. import javax.script.ScriptEngineManager;
  3. import javax.script.ScriptException;
  4. import java.lang.reflect.Method;
  5. import java.util.HashMap;
  6. import java.util.Map;
  7.     
  8. public class JsonSample {
  9.     
  10.     public static void main(String[] args) throws Exception {
  11.         // 起動時にオプションを指定しなかった場合は、このサンプルデータを使用する。
  12.         String script = "{ \"key1\" : \"val1\", \"key2\" : \"val2\", \"key3\" : { \"ckey1\" : \"cval1\", \"ckey2\" : [ \"cval2-1\", \"cval2-2\" ] } }";
  13.         if (args.length > 0) {
  14.             java.io.File f = new java.io.File(args[0]);
  15.             if (f.exists() && f.isFile()) {
  16.                 // 起動時の第1引数がファイルならファイルから読み込み(java 6 対応版なので、try-with-resources すら使えません。実際は、こんな書き方せずにちゃんとエラー処理してください)
  17.                 byte[] buf = new byte[new Long(f.length()).intValue()];
  18.                 java.io.FileInputStream fin = null; try { fin = new java.io.FileInputStream(f); fin.read(buf); } catch (Exception ex) { throw ex; } finally { if (fin != null) { fin.close(); }}
  19.                 script = args.length > 1 ? new String(buf, args[1]) : new String(buf); // ファイルの文字コードがシステムの文字コードと異なる場合は、第2引数で指定。例えば「UTF-8」や「JISAutoDetect」など。
  20.             } else {
  21.                 script = args[0]; // ファイルでなければ、第1引数の文字列をそのまま JSON として扱う
  22.             }
  23.         }
  24.         ScriptEngineManager manager = new ScriptEngineManager();
  25.         ScriptEngine engine = manager.getEngineByName("JavaScript");
  26.         // ScriptEngine の eval に JSON を渡す時は、括弧で囲まないと例外が発生します。eval はセキュリティ的には好ましくないので、安全であることが不明なデータを扱うことは想定していません。
  27.         // 外部ネットワークと連携するプログラムで使用しないでください。
  28.         Object obj = engine.eval(String.format("(%s)", script));
  29.         // Rhino は、jdk1.6,7までの JavaScript エンジン。jdk1.8は「jdk.nashorn.api.scripting.NashornScriptEngine」
  30.         Map<String, Object> map = jsonToMap(obj, engine.getClass().getName().equals("com.sun.script.javascript.RhinoScriptEngine"));
  31.         System.out.println(map.toString());
  32.     }
  33.     
  34.     static Map<String, Object> jsonToMap(Object obj, boolean rhino) throws Exception {
  35.         // Nashorn の場合は isArray で obj が配列かどうか判断できますが、特に何もしなくても配列番号をキーにして値を取得し Map に格納できるので、ここでは無視しています。
  36.         // Rhino だとインデックスを文字列として指定した場合に値が返ってこないようなので、仕方なく処理を切り分けました。
  37.         // 実際は HashMap なんか使わずに自分で定義したクラス(配列はそのオブジェクトの List プロパティ)にマップすることになると思うので、動作サンプルとしてはこんなもんでよろしいかと。
  38.         boolean array = rhino ? Class.forName("sun.org.mozilla.javascript.internal.NativeArray").isInstance(obj) : false;
  39.         Class scriptObjectClass = Class.forName(rhino ? "sun.org.mozilla.javascript.internal.Scriptable" : "jdk.nashorn.api.scripting.ScriptObjectMirror");
  40.         // キーセットを取得
  41.         Object[] keys = rhino ? (Object[])obj.getClass().getMethod("getIds").invoke(obj) : ((java.util.Set)obj.getClass().getMethod("keySet").invoke(obj)).toArray();
  42.         // get メソッドを取得
  43.         Method method_get = array ? obj.getClass().getMethod("get", int.class, scriptObjectClass) : (rhino ? obj.getClass().getMethod("get", Class.forName("java.lang.String"), scriptObjectClass) : obj.getClass().getMethod("get", Class.forName("java.lang.Object")));
  44.         Map<String, Object> map = new HashMap<String, Object>();
  45.         for (Object key : keys) {
  46.             Object val = array ? method_get.invoke(obj, (Integer)key, null) : (rhino ? method_get.invoke(obj, key.toString(), null) : method_get.invoke(obj, key));
  47.             if (scriptObjectClass.isInstance(val)) {
  48.                 map.put(key.toString(), jsonToMap(val, rhino));
  49.             } else {
  50.                 map.put(key.toString(), val.toString()); // サンプルなので、ここでは単純に toString() してますが、実際は val の型を有効に活用した方が良いでしょう。
  51.             }
  52.         }
  53.         return map;
  54.     }
  55. }

 以上で、完全に完結です。

 自動改行無し&横スクロール前提の、テキストエディタだとエラい見辛いソースで申し訳ないです。元々は、デバイスの画面幅がなんでも見れるように横スクロールは must、縦は 768 あればほぼ1画面内に収まるようにと、こんな書き方をしていた訳ですが、冒頭に起動時の引数処理を追加してしまった為、あんまり意味がなくなってしまいました。

 23行目まではどうでもいいので、24行目以降~横スクロールバー(PCの場合)が入るくらいの縦スクロール位置でご覧ください。お手数をおかけします。

 普通はこんな書き方したら怒られると思うので、皆様のコードはちゃんと整形してくださいね。内容自体は特に難しい箇所も無い短いコードなので、そのまま読み下していただければ。

 簡単に説明すると、起動時に引数を何も指定しなければ、「{ "key1" : "val1", "key2" : "val2", "key3" : { "ckey1" : "cval1", "ckey2" : [ "cval2-1", "cval2-2" ] } }」という JSON を HashMap に変換して、その toString() の結果を標準出力に出力します。

 で、その場合の実行結果は、こうなります(順不同)。

> java JsonSample
{key1=val1, key2=val2, key3={ckey1=cval1, ckey2={0=cval2-1, 1=cval2-2}}}

 key3 の値が、ちゃんとネストした Map になっているのが分かると思います。

 なるべく短く記述したかったので、配列の場合も処理を使いまわしてインデックスをキーにしたMapにしています(「ckey2」の値が、それです)。

 <String, Object>みたいな記述が許せない方も多いかと思いますが、実際は HashMap なんか使わずに自分で定義したクラス(配列はそのオブジェクトのListプロパティ)にマップ(今風に bind と言うべきでしょうか)することになると思うので、ここでちゃんと書いてもあんまり意味無いし、まぁ、とりあえず結果が見れればいいかなと。

 いちおう注釈しておきますが、これはそのままコピペして使うコードではなく、理解の一助となることを目的とした、単なるヒントです(そもそも、上の書き方では大きなデータは扱えませんし、パフォーマンスも考慮されていません)。ご自分のプログラムは、ご自身で適切に組み上げてください。

 それから、ソース内に JSON を記述しているのは、単にソースのみで完結した方がサンプルとして分かり易いかなというだけのことですので、実際に自分で組む場合は、もちろんデータの中身はなんでも構いません。

 と言うだけではなんですし、試しに起動時の引数でファイルや文字列を指定できるようにしてみましたので、RFC から持ってきたこんなのや(「java JsonSample json1.txt」で実行)、

 json1.txt { "Image": { "Width": 800, "Height": 600, "Title": "View from 15th Floor", "Thumbnail": { "Url": "http://www.example.com/image/481989943", "Height": 125, "Width": "100" }, "IDs": [116, 943, 234, 38793] } }

こんなのを読み込ませてみるのも良いでしょう(「java JsonSample json2.txt」で実行)。

 json2.txt [ { "precision": "zip", "Latitude": 37.7668, "Longitude": -122.3959, "Address": "", "City": "SAN FRANCISCO", "State": "CA", "Zip": "94107", "Country": "US" }, { "precision": "zip", "Latitude": 37.371991, "Longitude": -122.026020, "Address": "", "City": "SUNNYVALE", "State": "CA", "Zip": "94085", "Country": "US", } ]

 文字列を引数で指定する場合は、例えば次のように実行します。

> java JsonSample "{ \"key1\" : \"val1\", \"key2\" : \"val2\", \"key3\" : { \"ckey1\" : \"cval1\", \"ckey2\" : [ \"cval2-1\", \"cval2-2\" ] } }"

 まー要するに、データの中身はどうでもよくて、上記のコードは ScriptEngine.eval の戻り値である 27 行目の「obj」を如何にして自分に都合の良い Java オブジェクトに変換するかというサンプルであり、つまり主眼は jsonToMap メソッドです。

 Java SE 8(JDK 1.8)からは JavaScriptエンジンが Rhino から Nashorn に変わりましたが、いちおうどちらにも対応しています。

 動きが分かる程度の簡単なサンプルなので、セキュリティ対応や例外処理などはしていません。
 Engineも、Rhino と Nashorn にしか対応していません。汎用性を持たせるのであれば、クラス名等はプロパティで指定できるようにした方が良いでしょう。

 なんというか、自分で適当に reflection で調べただけなので、実際はもっと真面目に上手いやり方をしていただければ。

 いちおう、クラスパスを通すとかいう類いの環境設定作業を一切必要とせず、Java SE 6(JDK 1.6)以降がインストールされてさえいれば、何も考えずにコンパイル&実行できるようには記述してあります。

 ここまでくれば、上にあげたライブラリや .Net の System.Web.Script.Serialization.JavaScriptSerializer.Deserialize みたいなことも、割りと簡単に実装できると思います。

 そんな感じで、よろしくお願いします(何を)。

 ちなみに、逆(Java のオブジェクトを JSON テキストに変換)に関しては、普通にベタ書きで簡単に実現できるので、ここでは触れません。
 自前で実装しなくてはならない場合は、仕様に準じた形で好きなように組んだらいいんじゃないかと思います。JSON は簡単さがウリなので、RFC もめっちゃ短いですし。

 仕様については、参照するのは RFC 4627 じゃない方がいいと思います。2014 年時点の最新は RFC 7159 かな? 詳しくは、上の Google 検索リンクや、以下の記事などを参照してください(丸投げ)


2016/02/02 追記
 長らく書きっぱなしにしてしまってすみません。

 json.org のトップに記載されたように、現在 JSON は ECMA-404 によって定義されているようです。

 経緯が無駄に複雑で把握し切れていない部分があるのですが、Web ブラウザの実装に準じるのであれば、RFC 7159 より ECMA-404 を参照した方が良いのでしょう(まぁ、両方見れば済む話ですが)。

 記事をアップした当初は、ちょっと適当に書き過ぎたと反省しています。いや、昔からの流れを見ていたので、JSON ってそんなに肩肘張って定義するようなモンじゃないという気分が、個人的に強かったのです。申し訳ありませんでした。

2016/02/08 追記
 自分で追記しといてすっかり忘れてましたが、Java 9 から除外されてしまった JEP 198: Light-Weight JSON API では、RFC 7159 を参照してますね。だからなんだという話ですが、ご参考まで。

おわりに

 後から中途半端に直したので、JDK と Java SE の表記がブレブレですみません。この記事での Java は、特別な記述が無い限り Java SE(Standard Edition)のことを指します。

 それから、もちろん Rhino や Nashorn のオブジェクトをそのまま使っても構わないと思います。どちらかというと、上で紹介している方法こそが邪道な気もするので、ご参考程度に。実際は、お好きに組んでください。

この記事をシェア
  • このエントリーをはてなブックマークに追加
  • Share on Google+
  • この記事についてツイート
  • この記事を Facebook でシェア