Magnolia Tech

techっぽいことを書くブログです

Scalatra-Jsonがレスポンスを返す仕組み

前回のエントリで、Scalatraがレスポンスを返す仕組みについて紹介しました。Any型で返すのは型安全的にどうなの?という気持ちも有り、いつの日か変えたいという気持ちですが(Scalatra 3.0?)、現状はそんな仕組みになっています。

blog.magnolia.tech

Scalatra-JSON

前回説明した通り、ScalatraではAny型で受けたコードブロックの返り値を、パターンマッチで振り分けます。

NativeJsonSupport traitか、JacksonJsonSupport traitをmix-inすると、下記のrenderPipelineメソッドが有効になります。

github.com

github.com

Json4sがXMLをサポートしている関係で大量のXML関係のコードが含まれていますが、飛ばしながら読み進めていきましょう。

JValueResult traitが持つrenderPipelineメソッドがもっとも優先的に起動されます。対応するcaseが無ければ、orElse super.renderPipelineにより次のrenderPipelineへ処理が委譲されます。ここでのsuperが指す先はJsonOutput traitrenderPipelineメソッドになり、更にそのsuperが指す先はScalatraBase traitrenderPipelineメソッドになります。

JValueResult traitが持つrenderPipelineメソッドの一部を以下に引用します。Json4sのASTであるJValue型であれば、すぐにJsonOutput traitrenderPipelineへ移譲されていることが分かるでしょう。また、詳しくは後述しますが、case a: Any if isJValueResponse && customSerializer.isDefinedAt(a)によりJson ASTへ変換可能なcase classがJValue型として処理されていることが分かるでしょう。

  override protected def renderPipeline: RenderPipeline = renderToJson orElse super.renderPipeline

  private[this] def renderToJson: RenderPipeline = {
    case JNothing =>
    case JNull => response.writer.write("null")
    case a: JValue => super.renderPipeline(a)
    case a: Any if isJValueResponse && customSerializer.isDefinedAt(a) =>
      customSerializer.lift(a) match {
        case Some(jv: JValue) => jv
        case None => super.renderPipeline(a)
      }
    case status: Int => super.renderPipeline(status)

また、status: Intのように一旦JValueResult traitrenderPipelineで処理されるけど、すぐにsuperを呼び出しているパターンが有ることも分かるでしょう。

JsonOutput traitrenderPipelineは以下のようなコードになっていて、JSONPへ対応するコードが混じっていて分かりづらいですが、通常のJSONであればwriteJson(transformResponseBody(jv), writer)というコードでwriterに渡されていることが分かるでしょう。

override protected def renderPipeline = ({

    case JsonResult(jv) => jv

    case jv: JValue if format == "xml" =>
      contentType = formats("xml")
      writeJsonAsXml(transformResponseBody(jv), response.writer)

    case jv: JValue =>
      // JSON is always UTF-8
      response.characterEncoding = Some(Codec.UTF8.name)
      val writer = response.writer

      val jsonpCallback = for {
        paramName <- jsonpCallbackParameterNames
        callback <- params.get(paramName)
      } yield callback

      jsonpCallback match {
        case some :: _ =>
          // JSONP is not JSON, but JavaScript.
          contentType = formats("js")
          // Status must always be 200 on JSONP, since it's loaded in a <script> tag.
          status = 200
          if (rosettaFlashGuard) writer.write("/**/")
          writer.write("%s(%s);".format(some, compact(render(transformResponseBody(jv)))))
        case _ =>
          contentType = formats("json")
          if (jsonVulnerabilityGuard) writer.write(VulnerabilityPrelude)
          writeJson(transformResponseBody(jv), writer)
          ()
      }
  }: RenderPipeline) orElse super.renderPipeline

このように、Scalatraではコードブロックが返す値の型に応じて色々な処理へ分岐していること、新しい処理をカスケードして追加できることが分かってもらえたでしょうか。

case classへの対応

既にコードは出てきましたが、ScalatraではAnyで受けた値を、パターンマッチにより処理を振り分けています。また、Json4sにはcase classへのマッピング機能があります。そのため、Json4sの機能を使って、Json ASTへ変換可能なcase classを受け取った場合は、自動的にJson ASTへ変換して処理したいわけです(わざわざ呼び出し側でJson ASTに変換してから呼び出すのもアレなので)。

Json4sにはCustomSerializerという機能が用意されていて、これによりターゲットとしているオブジェクトがJson ASTに変換可能であるか判定することができます。

これは他のJsonモジュールにはない、Json4s特有の機能です。この機能があるため、Anyで返したオブジェクトがJson ASTに変換可能か、判断することができるようなっています。

Scalatra in Action

Scalatra in Action