0
votes

I am trying to implement a generic pattern with which to generate marshallers and unmarshallers for an Akka HTTP REST service using Argonaut, handling both entity and collection level requests and responses. I have no issues in implementing the entity level as such:

case class Foo(foo: String)

object Foo {
  implicit val FooJsonCodec = CodecJson.derive[Foo]

  implicit val EntityEncodeJson = FooJson.Encoder

  implicit val EntityDecodeJson = FooJson.Decoder
}

I am running into issues attempting to provide encoders and decoders for the following:

[
  { "foo": "1" },
  { "foo": "2" }
]

I have attempted adding the following to my companion:

object Foo {
  implicit val FooCollectionJsonCodec = CodecJson.derive[HashSet[Foo]]
}

However, I am receiving the following error:

Error:(33, 90) value jencode0L is not a member of object argonaut.EncodeJson

I see this method truly does not exist but is there any other generic method to generate my expected result. I'm strongly avoiding using an additional case class to describe the collection since I am using reflection heavily in my use case.

At this point, I'd even be fine with a manually constructed Encoder and Decoder, however, I've found no documentation on how to construct it with the expected structure.

2

2 Answers

1
votes

Argonaut have predefined encoders and decoders for Scala's immutable lists, sets, streams and vectors. If your type is not supported explicitly, as in the case of java.util.HashSet, you can easily add EncodeJson and DecodeJson for the type:

import argonaut._, Argonaut._
import scala.collection.JavaConverters._

implicit def hashSetEncode[A](
  implicit element: EncodeJson[A]
): EncodeJson[java.util.HashSet[A]] =
  EncodeJson(set => EncodeJson.SetEncodeJson[A].apply(set.asScala.toSet))

implicit def hashSetDecode[A](
  implicit element: DecodeJson[A]
): DecodeJson[java.util.HashSet[A]] =
  DecodeJson(cursor => DecodeJson.SetDecodeJson[A]
    .apply(cursor)
    .map(set => new java.util.HashSet(set.asJava)))

// Usage:

val set = new java.util.HashSet[Int]
set.add(1)
set.add(3)
val jsonSet = set.asJson // [1, 3]
jsonSet.jdecode[java.util.HashSet[Int]] // DecodeResult(Right([1, 3]))

case class A(set: java.util.HashSet[Int])
implicit val codec = CodecJson.derive[A]
val a = A(set)
val jsonA = a.asJson // { "set": [1, 3] }
jsonA.jdecode[A] // DecodeResult(Right(A([1, 3])))

Sample is checked on Scala 2.12.1 and Argonaut 6.2-RC2, but as far as I know it shouldn't depend on some latest changes.

Approach like this works with any linear or unordered homogenous data structure that you want to represent as JSON array. Also, this is preferable to creating a CodecJson: latter can be inferred automatically from JsonEncode and JsonDecode, but not vice versa. This way, your set will serialize and deserialize both when used independently or within other data type, as shown in example.

0
votes

I don't use Argonaut but use spray-json and suspect solution can be similar.

Have you tried something like this ?

implicit def HashSetJsonCodec[T : CodecJson] = CodecJson.derive[Set[T]]

if it doesn't work I'd probably try creating more verbose implicit function like

implicit def SetJsonCodec[T: CodecJson](implicit codec: CodecJson[T]): CodecJson[Set[T]] = {
  CodecJson(
    {
      case value => JArray(value.map(codec.encode).toList)
    },
    c => c.as[JsonArray].flatMap {
      case arr: Json.JsonArray =>
        val items = arr.map(codec.Decoder.decodeJson)
        items.find(_.isError) match {
          case Some(error) => DecodeResult.fail[Set[T]](error.toString(), c.history)
          case None => DecodeResult.ok[Set[T]](items.flatMap(_.value).toSet[T])
        }
    }
  )
}

PS. I didn't test this but hopefully it leads you to the right direction :)