Javaの標準的なJSON APIはJava JSON-P/JSON-Bがありますが、新しいJakarta EEでは Jakarta JSON Processing/Jakarta JSON Bindingになっています。
両方対応するのはどうするの、ということで変換機能を作ってみました。
もともとABNF Parser から JSON Parserを練成していたので元はそこから拡張してJava EEの JSON-P APIに準拠させています。JSON-Bの方っぽい機能もそろってきます。
拡張していくと Jakarta EE対応どうするの、という壁があります。JDK 8とJDK 11でバイトコードも違うので分けないとJDK 8ないろいろも死んでしまいます。
直接変換してもいいのですが、JavaのList/Mapなど経由でいろいろ拡張できる構成にしています。
Java Object(List/Map), JSON-P (Class), JSON Processing (Class), JSONテキスト, CBOR YAMLその他もろもろの型をJDBCドライバのように拡張しながら使うことができる SoftLibRebind とかいうものをつくります。
テキストのParserはSoftLibABNFなどまた別に作ってありますが、Objectの分解用機能と生成用機能を別々に作ってJavaのList/Map経由でいろいろ変換します。
- 本体 SoftLibRebind
- 拡張 SoftLibJSON, SoftLibJakartaJSON, SoftLibYAML...etc
Maven では
- groupId
- net.siisise
- artifactId
- softlib-rebind
- softlib-json
- softlib-jakarta.json
- softlib-yaml
などです softlib-jakarta.json のみ JDK 11以降
ここまでの流れを簡単に
- ABNF Parser作る
- JSON Parserをのせる
- JSON Binding的なものをのせてJava List/MapとJSONの行き来ができる
- Java Objectとも変換できる
- JSON-P/JSON-Bに少し合わせてみる
- Jakarta EEで使えるようJSON実装を3種類くらいに分け分けする
- Binding 的なものを分離する
- CBORとかYAMLとか一部対応する
- JSON テキスト出力をBinding経由にする
- Jakarta EEに対応する
出力拡張
サービスプロバイダ ( jar の META-INF/services ) の機能を使い、拡張を登録できる形にします。
こうすることでJDK 11以降対応などの機能は別に切り出すことができます。
元になる interface は net.siisise.bind.format.TypeFormat です。
たとえば null と 数値型 のデータをテキストっぽく変換するには
class TextFormat implements TypeFormat<String> {
public String nullFormat() {
return "null";
}
public String numberFormat(Number num) {
return num.toString();
}
}
のようなことになります。JsonGenerator をゆるくしたような感じで簡単です。 <> は出力型です。各データ型に対応したメソッドが出来上がれば完成。
数値型は Number でまとめてあるので将来的には分けたりするかもしれません。
META-INF/services/net.siisise.bind.format.TypeFormat に作ったclass名をフルで入れます。
ファイルに出力する場合は ContentBind, Object系に変換する場合は TypeBind を継承するとそれぞれ型から変換機能を使うことができるようになります。
ContentBind では contentType() で contentType を返すようにします。
TypeBind では targetClass() で出力するclass を指定します。
アノテーションにするかもしれません。
使い方
Rebind.valueOf( 元データ, 出力型class ); // Object / class用
Rebind.valueOf( 元データ, contentType ); // TEXT / Stream / ファイル用
を想定しています。
JsonValueな形式には Rebind.valueOf( obj, JsonValue.class ); でそれっぽく変換してくれます。
JSONのテキスト出力は Rebind.valueOf( obj, "application/json"); でStringになるかもしれません。型は実装次第です。
どちらも入力objはJsonValueではなくてもいいです。int, List, char[] いろいろあります。
ファイルにも出力形式がいろいろあるので組み合わせたりもできればいいかもしれない予定は未定。
出力TypeFormtを直接指定することもできます。
TypeFormat format = new TextFormat();
String txt = Rebind.valueOf( 元データ, format );
TypeFormat側でオプション設定や出力先を持っている場合などはこちらの使い方ができそうです。
入力拡張でバラバラ
入力側はどうなっているかというと、reflection などを使って当てはまる形式を順番に探していきます。
ファイル, Stream入力系はまだ対応していないので個別のParserなどを使います。
現状対応できそうなものはこんな感じですが、ファイルや文字列の中をParseしてJSONやBASE64など勝手に推測して変換するとIEのようになってしまうので型ぐらいは指定する必要がありそうです。
入力できるObject
- SoftLibRebind
- Java Primitive
- Array
- Map/List
- Java Object
- SoftLibJSON
- Java EE JSON-P ( JsonValue )
- SoftLibJakartaJSON
- Jakarta EE JSON Processing ( JsonValue )
Objectをバラすには net.siisise.bind.TypeUnbind を実装します。
class UnbindString implements TypeUnbind {
public Type[] getSrcType() {
return new Type[] { String.class };
}
public <T> T valueOf( Object src, TypeFormat<T> format ) {
if ( src instanceof String ) {
return format.stringFormat((String) src);
}
return this;
}
String を例にしてみます。
getSrcType() で対応できそうな候補classの一覧を指定します。
Unbind側にはTypeFormatもいっしょに渡されるので、srcをいい感じに分解してformat側に渡すことができれば任務完了です。
対応できない場合は自分自身を返すと次の候補のTypeUnbindに処理が移ります。これはnullなども出力形式としてあるのでnullを何もしなかった印にはできないためです。
個別でやっているのはこれだけです。入力型がStringで出力がInteger などの場合も階層化する場合にはあるかもしれません。
Objectの変換ではそういうこともしています。
TypeFormatはBindObjectを継承していないのでobjectFormatしたい場合は個別に継承されているかいないかを判定して出力形式を分けます。
Unbind側はまとめてnet.siisise.bind.UnbindListをつくってからサービスプロバイダに登録します。(省略)
というわけで、簡単に紹介してみました。
Jackson など他のAPIとの変換機能を追加してみたりするのもいいかもしれません。
https://github.com/okomeki/ でいろいろ公開しているよ。