Scala Circe with generics

假装没事ソ 提交于 2019-11-29 05:11:59
Idan Waisman

What you want is not going to work unless you can implement a strategy for turning any object into Json... which seems unlikely. Circe (and many other libs) instead choose to use a common pattern called Type Classes to make it convenient to define how you want to do something, in this case Encoder/Decoder, for a specific type.

I recommend researching Type Classes if you are unfamiliar with them. And then take a look at the Circe docs to see how you can implement Encoders/Decoders specifically.

mixel

Following Idan Waisman answer and C4stor answer in my duplicate question I used the Type Classes pattern. For brevity I provide sample code only for decoding json. Encoding can be implemented in exactly the same way.

First, let's define the trait that will be used to inject json decoder dependency:

trait JsonDecoder[T] {
  def apply(s: String): Option[T]
}

Next we define object that creates instance implementing this trait:

import io.circe.Decoder
import io.circe.parser.decode

object CirceDecoderProvider {
  def apply[T: Decoder]: JsonDecoder[T] =
    new JsonDecoder[T] {
      def apply(s: String) =
        decode[T](s).fold(_ => None, s => Some(s))
    }
}

As you can notice apply requires implicit io.circe.Decoder[T] to be in scope when it called.

Then we copy io.circe.generic.auto object content and create a trait (I made PR to have this trait available as io.circe.generic.Auto):

import io.circe.export.Exported
import io.circe.generic.decoding.DerivedDecoder
import io.circe.generic.encoding.DerivedObjectEncoder
import io.circe.{ Decoder, ObjectEncoder }
import io.circe.generic.util.macros.ExportMacros
import scala.language.experimental.macros

trait Auto {
  implicit def exportDecoder[A]: Exported[Decoder[A]] = macro ExportMacros.exportDecoder[DerivedDecoder, A]
  implicit def exportEncoder[A]: Exported[ObjectEncoder[A]] = macro ExportMacros.exportEncoder[DerivedObjectEncoder, A]
}

Next in the package (e.g. com.example.app.json) that uses json decoding a lot we create package object if does not exist and make it extend Auto trait and provide implicit returning JsonDecoder[T] for given type T:

package com.example.app

import io.circe.Decoder

package object json extends Auto {
    implicit def decoder[T: Decoder]: JsonDecoder[T] = CirceDecoderProvider[T]
}

Now:

  • all source files in com.example.app.json has Auto implicits in scope
  • you can get JsonDecoder[T] for any type T that has io.circe.Decoder[T] or for which it can be generated with Auto implicits
  • you do not need to import io.circe.generic.auto._ in every file
  • you can switch between json libraries by only changing com.example.app.json package object content.

For example you can switch to json4s (though I did the opposite and switched to circe from json4s). Implement provider for JsonDecoder[T]:

import org.json4s.Formats
import org.json4s.native.JsonMethods._

import scala.util.Try

case class Json4SDecoderProvider(formats: Formats) {
  def apply[T: Manifest]: JsonDecoder[T] =
    new JsonDecoder[T] {
      def apply(s: String) = {
        implicit val f = formats
        Try(parse(s).extract[T]).toOption
      }
    }
}

And change com.example.app.json package object content to:

package com.example.app

import org.json4s.DefaultFormats

package object json {
    implicit def decoder[T: Manifest]: JsonDecoder[T] = Json4SDecoderProvider(DefaultFormats)[T]
}

With Type Classes pattern you get compile-time dependency injection. That gives you less flexibility than runtime dependency injection but I doubt that you need to switch json parsers in runtime.

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!