Magnolia Tech

いつもコードのことばかり考えている人のために。

Json4sの使い方

前回のエントリからの続き。今回はざっとJson4sの使い方をおさらいする。

JSON文字列のパースと、生成

Json4sはバックエンドとしてJavaJSONライブラリであるJacksonを使うjacksonパッケージと、lift-json由来のパーサーを提供するnativeパッケージの2種類が用意されていて、どちらを使うかによってimportするパッケージが変わってくる。

例えばjacksonパッケージを使ってJSONをパースし、指定したデータ型にマッピングするときは、以下のようなコードとなる。

import org.json4s._
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.read

implicit val formats = Serialization.formats(NoTypeHints)

case class Person(name: String, age: Int)

val p = read[Person]("""{"name": "John", "age": 42 }""") # -> Person(John,42)

逆に、ScalaのデータからJSON文字列を生成する場合は、以下のようなコードとなる。

import org.json4s._
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.write

implicit val formats = Serialization.formats(NoTypeHints)

case class Person(name: String, age: Int)

val p = Person("John", 42)
val j = write(p) # -> {"name":"John","age":42}

上記の例ではjacksonパッケージを使ったが、nativeパッケージでも同様に動作する。

ただし、Jacksonのパーサーは頻繁にメンテナンスが行われているが、Json4sのパッケージ内でメンテナンスされているnative版パーサーは現在では決してきちんとメンテナンスされているとは言いがたく、不正なJSON文字列でもパースできてしまったり、RFC8259で定義されているobjectやarray以外のトップレベルのデータを正しくパースできないといった問題が有り、今ではJackson版を使っておく方が安全である。

JSON ASTを経由する

先ほどの例では直接JSON文字列をScalaのcase classにマッピングしたが、内部では一回JSON ASTを経由している。

次はJSON ASTを明示的に生成する方法を紹介する。

JSON文字列からJSON ASTを生成するためにはparseメソッドを使用する。

import org.json4s._
import org.json4s.jackson.JsonMethods._

val pa = parse("""{"name":"John","age":42}""") # -> JObject(List((name,JString(John)), (age,JInt(42))))

反対に、JSON ASTからJSON文字列を生成するためには、prettyメソッドか、compactメソッドを利用する。

val p = pretty(pa)

# {
#   "name" : "John",
#   "age" : 42
# }

val c = compact(pa)
# {"name":"John","age":42}

JSON ASTを経由する理由は、Json4sにはASTを直接操作するための豊富なメソッド(filterやmerge、query構文)が用意されていて、JSONをそのままScalaのデータ型にマッピングする前に一定の前処理をしておきたい、といった場合に有効である。詳しくはREADMEを参照のこと。

元々Json4sはScalaJSONライブラリに共通的に利用可能はASTを提供するために作られた経緯があり、豊富なASTレベルでの操作メソッドが用意されている。

おわりに

Json4sが用意するJSON ASTを操作するためのメソッド群は非常にたくさんの種類が用意されていますが、冒頭の例のように直接Scalaのデータ型にマッピングした方が扱いやすいと思いますので、まずはJSONScalaのデータ型を変換するorg.json4s.jackson.Serializationパッケージの使い方を覚えれば十分です。

次回は、JSON ASTからScalaのデータ型へ変換する方法について解説します。Json4sがランタイムリフレクションを使いまくっている様子をご紹介します。

Json4sについて、その機能と内部構造

Json4sの使い方と、内部構造についてのいくつかのこと

年明けからJson4sのメンテナーをやっている。

Json4sは、2012年に作られた歴史あるScala用のJSONライブラリで、Apache Sparkで使われていることもあり、割と広く使われている。しかし、ランタイムリフレクションをフルに使った(使いすぎた?)その非常に複雑な構造が足かせとなって、エッジケースの改修が全然追いついていない。そのため、今ではこんな評価となっている(このツイート自体ですら、4年も前…)。

とはいえ、まだまだ使われているし、古いissueが解決されないままになっているのも良くない。

元々、ScalatraのSwaggerライブラリのコードがJson4sベースだったということもあり、それなりにコードを読み込んでいたので、年末年始のまとまった時間が取れたタイミングで放置されてたissueにコメントしたり、PR作ったりしていたら、メンテナに入れてもらった。

あらためて、JSONについて

一度JSONについてあらためて復習する。


JSON(JavaScript Object Notation)の仕様は、RFC8259で定義されている。

JavaScript Object Notation (JSON) is a text format for the serialization of structured data. It is derived from the object literals of JavaScript, as defined in the ECMAScript Programming Language Standard, Third Edition [ECMA-262].

冒頭にこのように書かれている通り、JavaScriptに由来するデータ構造をシリアライズし、テキストで表現するための規格である。

また、同じ仕様がECMA-404としても規格化されている。

改めて最新のJSONの仕様を見てみると、エンコーディングは必ずUTF-8と決められ、トップレベルのデータ型がobjectやarrayに限定されない(現在では、nullや、"foo"だけでも有効なJSON)など、初期の頃から意外と変わっていることが分かる。

JSONのデータ構造

簡単におさらいすると、JSONには、object(辞書/連想配列), array(配列), string(文字列), number(数値), boolean(true or false), nullの6種類のデータ型が用意されている。

また、objectやarrayは複数のデータ型を混在して保持でき、かつobejctやarrayは任意の深さでネストできる。

例えば下記のようなデータ構造が作れる(objectの中に、arrayと真偽値の"true"と”null”が混在している)。

{
  "foo" : [ "fizz", "buzz", 42, {
    "wizard" : "Gandalf"
  } ],
  "bar" : true,
  "baz" : null
}

この”複数のデータ型が混在できる”、”任意の深さでobjectとarrayをネストできる"という特徴がScalaJSONを扱うときのポイントとなってくる。

各言語の組み込みのデータ構造や、コレクションクラスがサポートするデータ構造は当然JavaScriptと完全に互換性が有るとは限らない。つまり、JavaScriptの言語仕様をベースに作られたJSONを読み込むためにはJSONが表現するデータ構造を再現できる機能を作り込む必要がある。

PerlにおけるJSON

Json4sの解説に入る前に、JavaScriptと同様の動的型付言語であるPerlにおけるJSONの扱いを見てみる。

Perlのコアライブラリに含まれているJSONライブラリJSON::PPのコードを紹介する。このコードはJSON文字列をパースするコードの一部(余計なところは削除している)だが、文字列が'{'だったらobjectのはじまり、'['だったら配列の始まりと認識して、それぞれobjectをパースする関数、arrayをパースする関数がコールされていることが分かる。

sub value {
    return          if(!defined $ch);
    return object() if($ch eq '{');
    return array()  if($ch eq '[');
    return string() if($ch eq '"');
    return number() if($ch =~ /[0-9]/ or $ch eq '-');
}

実際に配列をパースするコードは以下のようになっていて、最初のmy $a = $_[0] || [];がポイント。引数無しで呼び出された時は、空の配列へのリファレンスを生成していることが分かる。

    sub array {
        my $a  = $_[0] || []; # you can use this code to use another array ref object.

        if(defined $ch and $ch eq ']'){
            --$depth;
            next_chr();
            return $a;
        }
        else {
            while(defined($ch)){
                push @$a, value();

                if($ch eq ']'){
                    --$depth;
                    next_chr();
                    return $a;
                }

その後、すぐに配列が']'で閉じられていれば、そのリファレンスを返却しているし、実際のデータが有ればそれを配列に格納(push)して、その配列(リファレンス)を返却している(next_chrは処理対象の文字を1文字ずつ進める関数、valueは続くデータをパースする関数)。

Perlは動的型付言語であり、組み込みのデータ構造としてハッシュ(JavaScriptのobjectに相当)と、配列(JavaScriptのarrayに相当)を持ちハッシュや、配列には複数のデータ型の混在が許される。また、リファレンスというデータ構造により、任意の深さでハッシュと配列とネストできる。

JSON::PPは、JSONのデータを以下のようにPerlのデータ型へ変換する。

JSONのデータ型 対応するPerlのデータ型
object ハッシュのリファレンス
array 配列のリファレンス
string 文字列
number 数値
boolean JSON::PP::Boolean
null undef

Perlには組み込みの真偽値型が存在しないので、if文の中で真偽として使えるJSON::PP::Booleanというオブジェクトで代替しているが、その他は比較的素直な対応となっている。

特にハッシュリファレンスと、配列リファレンスによりJSONのobjectとarrayがそのまま表現できているところがポイントとなる。

冒頭のJSONデータをJSON::PPで変換すると、以下のようなPerlのデータに変換される。JSONと同じように、複数のデータ型が混在していること、配列の中にハッシュが含まれていることが分かる。

$ref = {
          'baz' => undef,
          'foo' => [
                     'fizz',
                     'buzz',
                     42,
                     {
                       'wizard' => 'Gandalf'
                     }
                   ],
          'bar' => bless( do{\(my $o = 1)}, 'JSON::PP::Boolean' )
        };

通常のPerlのリファレンスなので、そのままPerlのリファレンスアクセスの構文で値を取り出すことができる。

print $ref->{'foo'}[2]; # prints 42

上記のことから、JSONPerlのデータ構造に移し替えるのは比較的容易であることが分かってもらえたと思う。

ScalaにおけるJSON

JavaScriptと同様に、組み込み型としてobjectやarrayに相当するデータ型を持ち、かつ動的にネストした構造が作れるPerlの場合、割と素直にJSONを言語組み込みのデータ形式に変換できた(真偽値だけは、固有のオブジェクトを利用したが)。

一方でScalaは静的型付言語であるため、事前に型は一つに決まっている必要がある。JSONを読み取りながら、そのデータの内容に応じて動的にネストしたobjectやarrayに相当するデータ構造を汎用的に作り出すことができない。

例えば、arrayを標準コレクションクラスに用意されているList[Int]マッピングすると、Int型のデータしか保持できない。おなじようにobjectをMap[String]マッピングすると、String型のデータしか保持できない。

Scalaに予め用意されているデータ型だけを使って混在させることは、できない。

もちろんあらかじめデータ構造を決め打ちにすればできるが、それでは汎用性が失われる。また、全部Any型にしてしまえばできなくはないが、それでは型情報が失われ、Scalaのメリットが失われてしまう。

つまり、Scalaの標準のコレクション型ではJSONが前提とするデータ構造を素直にマッピングすることはできない。

では、どうするか?

上記のListMapはあくまでScalaの標準コレクションライブラリが提供するクラスであり、そのクラスが表現できないデータ構造が必要であれば、作れば良い。

Json4sは、ScalaのクラスとしてJSONの持つ情報量を保持したままScalaであつかえるようなデータ構造、AST(Abstract Syntax Tree)を提供している。

Json4sが提供するASTと、JSONとの対応は以下の通りとなる。

JSONのデータ型 対応するJson4sのデータ型
null JNull
string JString
number JNumber
boolean JBool
array JArray
object JObject
- JNothing

最後に追加されているJNothingは、OptionのNone相当であり、JSON文字列のパースに失敗した時などにJson4sが返却するデータ型。

また、実際の定義を抜粋すると、以下のようになる。特に、JObjectやJArrayがどうやって元のJSON情報を保持しているか、よく分かると思う。

object JsonAST {
  sealed abstract class JValue extends Diff.Diffable with Product with Serializable

  case object JNothing extends JValue
  case object JNull extends JValue
  case class JString(s: String) extends JValue
  
  trait JNumber
  case class JDouble(num: Double) extends JValue with JNumber
  case class JDecimal(num: BigDecimal) extends JValue with JNumber
  case class JLong(num: Long) extends JValue with JNumber
  case class JInt(num: BigInt) extends JValue with JNumber {
  case class JBool(value: Boolean) extends JValue {

  type JField = (String, JValue)
  case class JObject(obj: List[JField]) extends JValue
  case class JArray(arr: List[JValue]) extends JValue
}

Scalatra 2.7がリリースされた

自分がメンテナンスに参加しているScala用のSinatra-likeなWeb Application Framework、Scalatraの最新バージョンがリリースされた。

scalatra.org

今回のリリースの最大の特徴は、Scala 2.13対応だけど、それ以外にも古くて使われていない機能をバシバシ削除してメンテナンス性を向上させています。

詳しくはリリースノートを見てください。

自分でも覚えられないくらいPRを出したので、もし問題が有ったら自分が書いたコードの部分かもしれないので、問題を見つけたらGitHubにissueを上げてください。

ちゃんとフォローします!

Scalatra in Action

Scalatra in Action

「教養としてのコンピューターサイエンス講義 」を読んだ

日常的にコンピュータを手足のように扱い、コードを書いて、やりたいことを実現している人たちからすれば当たり前のことが、一般の人たちからすれば全然当たり前のことではなかったりすることが、まれによくある。

ブライアン・カーニハンの「教養としてのコンピューターサイエンス講義 」は、一般人向け「コンピューターサイエンス」の講義の内容が書籍化されたもので、コンピュータを取り巻くさまざまなことが「なぜそうなったか?」「どうしてそうあるべきなのか?」という視点と、豊富な歴史的経緯や周辺情報と共に、圧倒的な情報量で語られる。

ハードウェア、ソフトウェア、そしてプログラミング…ネットワークや、インターネット、検索エンジン、セキュリティなどなど、とにかくなにげなく使っているコンピュータの、それを支える仕組みがざっとわかるようになっていて、一般の人だけでなく、コードを書くような人たちも、自分の知識のベンチマークとしてもちょうど良い構成になっている。

自分の知識がどれだけちゃんと背景をふまえたものか、どれだけの網羅性を持っているかを確認するためにも、あるコンピュータの概念を一般の人に説明するときにどこからどのくらいの粒度で説明すればいいのかを理解するためにもちょうど良い一冊です。

ちなみに、60ページの猫の写真が最高なので、それだけでもぜひ見てみてください。


2022/4に第2版が出版されたので、リンク先を更新...ちょうど読み返そうと思っていたところだったので、書い直そうかな

「ドメイン駆動設計入門」を買って、読んだ

先日開催されたObject Oriented Conferenceに象徴されるように、最近設計論の議論が盛んでですね。設計論と言えば、「エリック・エヴァンスのドメイン駆動設計」、いわゆるDDD本がよく取り上げられてきましたけど、なかなかヘビーな本だし、案外コードは全然出てこないので、読んだ上で「で、どうすればいいの?どんなコードを書けばいいの?」という疑問がわきます。

おなじくドメイン駆動設計の解説書である「実践ドメイン駆動設計」も、語られる順番がDDD本と変えることで併せて読むことで理解を深めることを意図していましたが、やはりコードの少なさは同じくらいでした。

ドメイン駆動設計入門」は、とにかく豊富なコード例が出てくるところが前述の2冊と異なるところです。特に一番わかりやすい「値オブジェクト」の詳細な解説はドメイン駆動設計入門の最初の一歩として非常に効果がわかりやすいですね。まずこの本を読んでから、DDD本や、実践本を読むのは良い入り方だと思います。


自分の中でこの本を読んでて思ったのはデータストアとの関わり方ですね。これまでアプリケーション構造に最も影響を与える存在がデータストアだったと思っていて、果たして本当にそのレイヤーを正しく抽象化して分離できるのか?その抽象化は正しいのか?という所はまだ答えがないですね。

SQLにたくさんのビジネス要求が詰まっているコードも多いですし、どこにドメイン知識を寄せていくのか?という時に、データストア固有のアクセス方法はまだ無視できるほどにはなっていないと思っていて、この辺の抽象化の考え方はまだまだ理想と現実があるなーって思いました。


とはいえ、ドメイン駆動設計は設計観点の一つの過ぎなくて、別に何か正解を示してくれる訳ではないので、ばんばんコードを書いて、「自分は何の課題を解決したいの?そのためにはどんな手法が良いとされているの?」というところは考えていくしかないんですけどね!そんなことを考えるきっかけとしても「ドメイン駆動設計入門」はおすすめです。


エリック・エヴァンスのドメイン駆動設計

エリック・エヴァンスのドメイン駆動設計

実践ドメイン駆動設計

実践ドメイン駆動設計

MacBook Proを修理した

というわけで日曜日に修理に出して、翌金曜日に修理が完了したので、足かけ2週間もPCがまともに使えない状態で、いろいろな活動ができなかった…(そんな時に限って吉祥寺.pmを中止にするとかっていうイベントも発生してしまった)。

ディスプレイも交換になってしまったので、液晶フィルムの張り直しになったけど、これが結構手こずった。毎回なかなかうまくいかない…

それでもパワーサポートのフィルムは位置合わせがやりやすくておすすめです。

とにかくApple Careには必ず入っておこう!水濡れは2回まで定額で直してもらえるよ!

「リファクタリング第2版」を読んだ

初版、たぶん読んだはずだけど、全然覚えていないので第2版を購入。JavaScriptには慣れていないけど、特に読む上で支障は無かったです。

かなり分厚い本なので、最初から読むとけっこう挫折してしまうかも…特に大部分はリファクタリング作業のカタログ集なので、頭から読むにはちょっと適していない構成です。

ある程度リファクタリング的な作業を経験した人であれば過去にやったことの有る作業を拾い上げて、自分の暗黙知形式知にしていく読み方がお勧めですね。

まだリファクタリングを経験したことが無い人だったら、迷わず第10章から読むことをお勧めします。複雑で難解なコードは、複雑な条件式(ifとかswitchとか)が入り組んでいることが多いので、それをシンプルに読みやすく書き下す方法が有る、ということを学ぶためにもこの章の「条件記述の分解」「条件記述の統合」「ガード節による入れ子の条件記述の置き換え」はとても学びが多いですね。あと、「アサーションの導入」も最初に学んだ方がいい事項と言えるでしょう。

ポリモーフィズムによる条件記述の置き換え」は、複雑なコードを単機能に分解するには有益ですが、ちょっと改修範囲が大きくなりすぎますしね。

第10章以外は、導入するには影響範囲もそれなりに大きいものが多いので、関数に閉じて適用できる第10章がお勧めです。まずは小さいところから始めましょう。

あとは第3章の「コードの不吉な臭い」、続く第4章の「テストの構築」と続けて読むと理解が深まると思いました。

とにかくダメなコードを指して、なぜダメなのか?ということを理解するのが大事ですね。先人たちが必ずしも正しいわけではなく、たまたまそうなっただけなのに、それを鵜呑みにするのは良くなくて、歴史的な経緯に敬意を払いつつ、ダメなコードはダメ、という姿勢でいきたいですね。

ただ、「コレクションクラスのカプセル化」は、昨今のプログラミング言語におけるコレクションライブラリが非常に高機能になっている中、割とシンプルなコレクションクラスを想定して書かれているような印象でした。すべての操作をカプセル化することはできないですが、主に参照のために用意されているさまざまなメソッドを、単にコピーを返す、という方法だけで良いかは結構議論が分かれる気がします。

ざっと読み進めておいて、必要になった時、自分の経験と一致するようなトピックがあるときなどに読むと良いですね。