json4sのScala 3対応やjson4sの内部アーキテクチャや歴史の簡単な解説

json4sという古代からあるScalajsonライブラリのメンテナンスをなぜか数年ずっとやっています。それの話をします。

前提として、メンテナンスはしていますが、個人的には基本的に使いたくないし、他の人にも全くおすすめしません。

あえて使うとしたら

  • なにかで、偶然json4sの依存がついてきて
  • ちょっとした処理をするだけだし、わざわざ別の依存追加せずにjson4s使ってしまえばいいか

というすごく雑に悪い意味で?手抜きしたいパターンでしょうか。その場合でも基本的におすすめしませんが。

すごく大昔にもっと優れたjsonライブラリが全く存在しない時、json4sが誕生した10年以上前には、json4sが一番マシな時期があったので、ある意味少しだけ流行って使われた時期がありました。

しかし現在のScala界隈にはもっと優れたjsonライブラリが大量にあるため、繰り返しになりますが、全くオススメできません。

あえてメンテしてるのは、自分の勉強のためと、Apache Sparkでまだ使われてる、という理由ですかね。

そういえばgitbucketもjson4sのreflection機能使ってしまっているが、やめたい・・・ (gitbucketもメンテナーしてます)

内部構造の観点からの、オススメしない大きな要因としては

  • 全体的な設計が古くて微妙である
  • 独自parser一部バグってる?
  • Scala 3対応が微妙

などです。

すごく細かいことを含めると他にも色々あり得ますが、とりあえずそれは割愛します。

「JNothingという謎の概念」というのは、大昔のplay-jsonもほぼ同様のミスをしていて( JsUndefined というのがあり、それが JsValue のsub classだった) しかし、大掛かりな変更をしてそれはplay-jsonでは解消されています。

json4sでは今更互換を大幅に壊してまで書き換えるのが、かなり面倒というか、 どうせjson4s使ってる人なんて今更最近のイケてるScalaライブラリで頑張りたくない人が仕方なく?よくわからずに?使ってる側面があり得る?という偏見があるため、 少なくとも自分はやる気はありません。

「型クラスではなくreflectionに頼ったシリアライズやデシリアライズ」 というのは、大昔は多少他のjson以外のライブラリでも流行ったのですが(squerylというdatabaseライブラリなど)、今となってはかなり珍しい部類に入りますね。 これが

  • バリバリにScalaの内部実装依存になる
  • 型クラスの方法と比較すると、原理上どうしても実行時エラーになる可能性が増して安全ではない場合がある
  • Scalaの内部実装依存なのでScalaのversionが上がったら動かなくなったり微妙な挙動の差異があり得る
  • これのScala 3対応が今まで全然できていなかった(今回ある程度は対応したがまだ課題が多い)

などなど、デメリットが大量です。 このreflectionの手法が出現して多少流行ったのはScala 2.9以前で、まだマクロすら存在しなかった、という時代背景があるかもしれません。

reflection問題については、多少、申し訳程度に最低限の型クラスの仕組みを作ったり、reflection部分とそうでないASTの部分やparserの部分のjarを分けたりして、数年前に多少改善したのですが、あくまで多少改善しただけあって、JNothingが微妙問題もあったりするし、型クラス使うなら結局json4sではなく他のjsonライブラリでいいのでは???ともなりがちです。

最後の

Scala 3対応が微妙」

については、Scala 3でbuildしてpublishという意味においては、Scala 3が出た2021年にすぐ実行できていました。

github.com

しかし、あくまでbuildをしただけであって、reflection関連部分は通っていないテストが大量にありました。

https://github.com/json4s/json4s/blob/4bc117f8623720ef1d26034b3dd648336d848f28/scala_3_test_filter.sbt

これは、Scala 2から3において、Scala独自の型情報を保持する仕組みが、ゼロから作り直されて完全に変わって、そこの対応が難しかったからです。

Scala 2までは、細かい変更はあれど、以下のアノテーションにメタ情報を埋め込んでいます。

しかし、Scala 3ではそこに埋め込むのをやめて .tasty という独自フォーマットのファイルが、JVMのclassファイルとは別に作られるようになり、そこに情報が入っています。 保持される場所が変わった、というだけではなく、保持される際の内部の形式も完全に変わっています。

Scala 2までは、Scala compiler内部にそのアノテーションを読み取るclassがあり、大昔のjson4sはscala-compilerに直接依存していましたが、10年近く前(!?)に、

github.com

必要な部分だけ埋め込んで、Scala 2のcompiler依存を消した、という経緯があります。

さて、色々と歴史を語ってしまいましたが、つい最近、ある程度完了したScala 3対応についてです。

Scala 3.0.0が最初にリリースされてから、もう3年も経過して、ようやくですね。

そもそも、実装したのは自分ではなく、他の人からの以下のpull reqです。

github.com

最低限のメンテナンスはしているけれど、Scala 3のreflection部分の対応は、(よほど慣れていないと)結構難しそうなので、(よほど偶然気が向くか、暇にならない限り)自分ではやる予定がありませんでしたが、pull reqもらいました、ありがたいですね。

現状でScala 3のtastyを"実行時に"読み取る方法の候補としては、例えば以下でしょうか

  • compileに依存
    • scala3-staging使う方法
    • tasty-inspector使う方法
    • compiler直接?その他?
  • tasty-query使う方法?
  • 他にもreflection関連ライブラリあったような(全て読み取れるのか、どの程度の強さなのか全く知らない) https://github.com/zio/izumi-reflect
  • その他?完全独自?(よく知らない)(他にもあったらコメントください)

"候補"といったのは、現在、どういう用途の場合に、どれが一番優れているのか、使い分けた方がいいのか?よく知らないし、 そもそもtasty-queryの完成度や、tasty-queryとcompiler依存を比較した場合に、出来ることに違いがあるのか?など、何も詳細知りません。

それの詳細は書けないので置いておく?として、今回もらったpull reqではscala3-stagingで実装されていました。

stagingの本来の用途というよりは、json4sの場合は、とにかくQuotesが実行時に手に入って欲しい(実行時reflectionがしたい)という用途のはずです。

自分も以前に似たようなことを別の方法でおこなって、色々考えたことはあります

pull reqにも書いてありますが、scala3-staging遅いのでは?とか、Scala 3.3のLTSではバグに遭遇して3.4にしないとうまく動かないパターンがある、 など、なんとなく大体が動くようになったとはいえ、色々と難題だらけです。

Scala 3 compilerの依存が丸ごとついてくるのも、明らかなデメリットですね。

繰り返しになりますが

「なんとなく大体が動くようになったとはいえ、色々と難題だらけ」

という点からも、json4sがオススメできない理由でもあります。

とはいえせっかくpull reqもらったのでbuild多少手直ししてmergeしてpublishしました。

ごく一部のlazy valなどのテストだけまだ動かなくてpendingにしてるものもありますが、基本的なパターンではScala 3でもreflection使った実装が動くようになったはずです。 (ただし、上記に書いたようにScala 3.3では無理で、現状は3.4以降が必須)

また、json4sのScala 3対応という観点では、以下のManifestに依存している問題も残っています

github.com

警告が出るだけで、現状のScala 3ではまだ動くのですが、遠い将来?にScala 3側でこの機能が打ち切られたら動かなくなってしまうはずなので、何かjson4s側で頑張る必要があるかもしれません。 とはいえ、かるく考えてみた限り、このManifest置き換えも結構面倒な気がしていて、自分ではあまりやるつもりがないというか、簡単に出来る気はしません。

やはり色々と不安だらけですね・・・。

最初にも書きましたが、自分が優先度高めで解決するべき課題かどうか?意味がある課題か?はともかく、他のライブラリ普通にメンテしてるだけでは遭遇しない謎に難易度が高い珍しい問題に遭遇するので、あえてメンテしてる、という側面があるので、今後も飽きなければ最低限のメンテは続けるかもしれません。