マイクロアドアドベントカレンダー20日目の記事です。
マイクロアドで内定者アルバイトエンジニアをしています。
何らかのjson文字列を受け取りcase classにマッピングする際に、値の文字列をそのまま文字列としてマッピングして欲しくなかったり、何らかの処理を加えてからマッピングしたいということもあると思います。そんな時にjson4sのCustomSerializerを使うと、簡単に実装することが可能です。
前提
Scalaのバージョンと、json4sのバージョンは以下のようになります。
scalaVersion := "2.11.7"
...
libraryDependencies ++= Seq("org.json4s" %% "json4s-native" % "3.2.11")
CustomSerializerの実装
例として、以下のようなJSONを想定します。scala側では"belongings"の値はSetで持っておきたいという気持ちがあります。belongings
の値としてそもそもカンマ区切りで送ってくるなみたいなところはあります。
{
"name": "taro",
"email": "taro@email.com",
"belongings": "ご飯,味噌汁,パン"
}
このようなJSONが送られてくると、belongings
は通常のjson4sのパースでは単なるStringとなるので、Setとして扱いたい時などは一度case classにStringとしてマッピングした後に別のclassに変換したりしなければなりません。
そこで以下のように、CustomSerializer[A: Manifest]
を継承したUserSerializer
を作成します。
case class User(name: String, email: String, belongings: Set[String])
class UserSerializer extends CustomSerializer[User](implicit formats => ({
case jsonObj: JObject =>
val name = (jsonObj \ "name").extract[String]
val email = (jsonObj \ "email").extract[String]
val belongings = (jsonObj \ "belongings").extract[String].split(",")
User(name, email, belongings)
}, {
case user: User =>
...
}))
こうして作成したクラスのコンストラクタにser: Formats => (PartialFunction[JValue, A], PartialFunction[Any, JValue])
となる関数を渡します。シリアライズ、デシリアライズのためのフォーマットを引数として受け取り、シリアライズ、デシリアライズの為の部分関数をタプルで返すような関数です。
今回はデシリアライズのみを目的にしているのででシリアライズの為のPartialFunction[JValue, A]
のみ実装してます。最初にcase jsonObj: JObject
とすることで、パースされた全ての入力にマッチします。
公式ドキュメントには載ってますが実はこの部分を
case JObject(JField("start", JInt(s)) :: JField("end", JInt(e)) :: Nil) =>
new Interval(s.longValue, e.longValue)
みたいにすると、入力のJSON構造にマッチしてデシリアライズを行うことが可能です。しかしマッチしない場合には例外が吐かれるので、不安定な構造のJSONを取り扱う時には注意が必要です。
また、コンストラクタ引数であるformats
をimplicit
で受け取ることで、特に意識せずに部分関数内でextract[A]
やextractOpt[A]
を使って値の抽出が可能になります。
\演算子
\
演算子を用いると、JObject
からjsonObj \ "key"
のようにして"key"以下のJValueを部分抽出することが可能です。
scala> jsonObj \ "belongings"
res: org.json4s.JValue = JString(ご飯,味噌汁,パン)
extract, extractOpt
extractはJValueが存在しない時(JNothingの時)には例外を吐きますが、extractOptはJNothingの時であっても例外を吐きません。
scala> (jsonObj \ "belongings").extract[String]
res: String = ご飯,味噌汁,パン
scala> (jsonObj \ "nothing").extract[String]
org.json4s.package$MappingException: Did not find value which can be converted into java.lang.String
at org.json4s.reflect.package$.fail(package.scala:95)
at org.json4s.Extraction$$anonfun$org$json4s$Extraction$$convert$2.apply(Extraction.scala:704)
at org.json4s.Extraction$$anonfun$org$json4s$Extraction$$convert$2.apply(Extraction.scala:704)
at scala.Option.getOrElse(Option.scala:121)
at org.json4s.Extraction$.org$json4s$Extraction$$convert(Extraction.scala:704)
at org.json4s.Extraction$$anonfun$extract$6.apply(Extraction.scala:394)
at org.json4s.Extraction$$anonfun$extract$6.apply(Extraction.scala:392)
at org.json4s.Extraction$.customOrElse(Extraction.scala:606)
at org.json4s.Extraction$.extract(Extraction.scala:392)
at org.json4s.Extraction$.extract(Extraction.scala:39)
at org.json4s.ExtractableJsonAstNode.extract(ExtractableJsonAstNode.scala:21)
... 43 elided
scala> (jsonObj \ "belongings").extractOpt[String]
res: Option[String] = Some(ご飯,味噌汁,パン)
scala> (jsonObj \ "nothing").extractOpt[String]
res: Option[String] = None
JSONの構造が不定の時に活用するといいでしょう。
使い方
作成したCustomSerializerを使う時には、デシリアライズ後のクラスがcase classならばobjectにパース用の関数を持たせ、そこでJsonMethods.parse()
を実行すると良さそうです。
この時、implicit lazy val formats: Formats = DefaultFormats + new UserSerializer()
のように、作成したCustomSerializerをformatsに加えるのを忘れないようにしましょう。
object User {
implicit lazy val formats: Formats = DefaultFormats + new UserSerializer()
def fromJson(value: String): User =
JsonMethods.parse(value).extract[User]
}
まとめ
処理の流れとしては、
-
\
演算子とextract[A]
、extractOpt[A]
で目的の値を抽出。 - 抽出した値をcase classに対応する形に加工して、ファクトリによって作成したインスタンスを返却。
という風になると思います。
これで入力のJSONがクソフォーマットでもScalaで楽に扱えそうですね。
(元々CustomSerializerは、サポートしていない型をシリアライズする為の機能っぽいので、マッピング先のクラスはcase classでなきゃいけない訳じゃありません。case classは標準で自動抽出に対応しているので、自動抽出では取り扱えないようなJSONをマッピングする時に使いましょう)
参考文献
- json4s - http://json4s.org/
- Json4s custom serializers - the right way - https://nmatpt.com/blog/2017/01/29/json4s-custom-serializer/