Using Argonaut to create generic JSON converter

1.6k Views Asked by At

I'm new to Scala, and here I'm trying to create a generic json converter based on Argonaut. I've tried to search on google and stackoverflow, but so far I have no clue.

Here is the snippet of my code.

import org.springframework.http.converter.AbstractHttpMessageConverter
import org.springframework.http.{MediaType, HttpInputMessage, HttpOutputMessage}    
import scala.io.Source
import argonaut._,Argonaut._

case class Currency(code: String)
object Currency {
    implicit def CurrencyCodecJson: CodecJson[Currency] = casecodec1(Currency.apply, Currency.unapply)("code")
}

case class Person(firstName: String, lastName: String)
object Person {
    implicit def PersonCodecJson: CodecJson[Person] = casecodec2(Person.apply, Person.unapply)("firstName", "LastName")
}

class ArgonautConverter extends AbstractHttpMessageConverter[Object](new MediaType("application", "json", Charset.forName("UTF-8")), new MediaType("application", "*+json", Charset.forName("UTF-8"))) {
    val c = classOf[Currency]
    val p = classOf[Person]

    def writeInternal(t: Object, outputStream: OutputStream) = {
        val jsonString = t match {
            case c:Currency => c.asJson.ToString()
            case p:Person => p.asJson.ToString()
    }

    def supports(clazz: Class[_]): Boolean = clazz.isAssignableFrom(classOf[CodecJson])// clazz == classOf[Currency] || clazz == classOf[LegalEntity]

    def readInternal(clazz: Class[_ <: Object], inputStream: InputStream): Object = {
        val jsonString = Source.fromInputStream(inputStream).getLines.mkString
        val jsonObject = clazz match {
            case `c` => jsonString.decodeOption[Currency]
            case `p` => jsonString.decodeOption[Person]
        }
        jsonObject match {
            case Some(j) => j
            case None => null
        }
    }
}

What I'm trying to do is to generalize such that I don't need to keep adding the match for every new model class (like Currency and Person in this case) that I will add in the future.

2

There are 2 best solutions below

1
On

you don't need single bean to handle every possible class, you can create a bean for each class, e.g.

class ArgonautConverter[T: CodecJson : ClassTag] extends AbstractHttpMessageConverter[T]    {    
  def supports(clazz) = clazz == classTag[T].runtimeClass
}
1
On

Argonaut already has generic encoding and decoding functions.

For example, Parse.decodeOption will parse a String into any type for which you have a codec.

What you are trying to do is decide at runtime whether you have a codec for a type, but you can make the compiler figure that out for you.

Whether you can decode to a type T depends on whether there is an implicit instance of DecodeJson[T] in scope. (That is a supertype of CodecJson[T], and you have written a couple of those, so they're good.)

Unfortunately, the compiler won't infer this constraint for you, so you have to mention it in the type signature. One way to do this is to use a context bound, which is the T : DecodeJson in the example below.

def read[T : DecodeJson](inputStream: InputStream): Option[T] = {
  val jsonString = Source.fromInputStream(inputStream).getLines.mkString
  Parse.decodeOption(jsonString)
}

(Also, note that you should really return Option[T] instead of using null.)

Similarly, write could be implemented with the signature:

def write[T : EncodeJSON](t: T, outputStream: OutputStream)

Your CodecJson[T] instances are also instances of EncodeJson[T].