Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala Circe with generics

Tags:

json

scala

circe

I am trying to use the scala json library Circe, wrapping it in a simple trait to provide conversion to/from json for which I have the following:

import io.circe.generic.auto._
import io.circe.parser._
import io.circe.syntax._

trait JsonConverter {
  def toJson[T](t : T) : String
  def fromJson[T](s: String) : T
}

case class CirceJsonConverter() extends JsonConverter{
  override def toJson[T](t: T): String = t.asJson.noSpaces
  override def fromJson[T](s: String): T = decode[T](s).getOrElse(null).asInstanceOf[T]
}

The aim of this is to simply be able to call JsonConverter with any object and have it convert it to/from json as such jsonConverter.toJson(0) must equalTo("0") , however when I try to compile it I get the following:

[error] could not find implicit value for parameter encoder: io.circe.Encoder[T]
[error]   override def toJson[T](t: T): String = t.asJson.noSpaces
[error]                                            ^
[error] could not find implicit value for parameter decoder: io.circe.Decoder[T]
[error]   override def fromJson[T](s: String): T = decode[T](s).getOrElse(null).asInstanceOf[T]
[error]                                                     ^
[error] two errors found

I can of course have a class that everything I intend to put through the converter inherit from, but I had the impression that circe could auto generate the encoders/decoders?

like image 973
RichyHBM Avatar asked Feb 07 '23 02:02

RichyHBM


2 Answers

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.

like image 58
Idan Waisman Avatar answered Feb 08 '23 15:02

Idan Waisman


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.

like image 27
mixel Avatar answered Feb 08 '23 15:02

mixel