Implicit jsonFormat for case class with varargs
Asked Answered
G

3

6

I have a case class containing varargs, with an implicit jsonFormat as follows:

import spray.json._
case class Colors(name: String*)
object MyJsonProtocol extends DefaultJsonProtocol {
  implicit val colorFormat = jsonFormat1(Colors)
}
import MyJsonProtocol._
Colors("CadetBlue").toJson

It raises an error:

error: type mismatch;
found   : Color2.type
required: Seq[String] => Color2
Note: implicit value colorFormat is not applicable here because it comes after the application point and it lacks an explicit result type
      implicit val colorFormat = jsonFormat1(Color2)
                                            ^

I have also tried:

implicit val colorFormat = jsonFormat1(Colors.apply)

which caused a different (runtime!) exception:

java.lang.RuntimeException: Cannot automatically determine case class field names and order for 'Colors', please use the 'jsonFormat' overload with explicit field name specification

The following:

implicit val colorFormat = jsonFormat(Colors, "name")

raises the former error

It is even possible to define implicit jsonFormat for case class with varargs?

Greaseball answered 29/1, 2015 at 10:16 Comment(2)
Have you already considered using a Seq[String] instead?Pompeii
Use List[String] instead of varargs, or make your own unmarshaller.Droit
K
5

It should work perfectly, probably you have some ambiguity in implicits. This works perfectly:

import spray.json._, DefaultJsonProtocol._

case class Test(param: String*)
object Test {
  implicit val testFormat = jsonFormat1(Test.apply)
}

Like a best practice advice, don't use Protocol pattern, it leads to large implicit errors in big project, always define implicits in your companion object (of cause there are exception cases). Another point avoid inheritance, it's not really needed.

Scala * pattern is just a sugar for Seq type constructor, so it should find seqFormat (un)marshaller for this cases.

Update

It doesn't work because Spray uses ClassManifest to extract field names from the copy function, but the compiler doesn't generate this function for case classes with varargs in constructor:

case class Test(params: String*)
def man[A: ClassManifest] = implicitly[ClassManifest[A]]
man[Test].erasure.getDeclaredMethods.filter(_.getName.contains("copy"))
res4: Array[java.lang.reflect.Method] = Array()

scala> case class Test(param: String)
defined class Test

scala> man[Test].erasure.getDeclaredMethods.filter(_.getName.contains("copy"))
warning: there was one deprecation warning; re-run with -deprecation for details
res5: Array[java.lang.reflect.Method] = Array(public Test Test.copy(java.lang.String), public java.lang.String Test.copy$default$1())

So you need to provide the field name manually. BTW i didn't know this before

Kumquat answered 30/1, 2015 at 8:51 Comment(5)
Tried in the scala console: error: value apply is not a member of object Test \n Note: implicit value testFormat is not applicable here because it comes after the application point and it lacks an explicit result typeGreaseball
@Greaseball you need to switch to ":paste" mode then eval itKumquat
Now there is an error: java.lang.RuntimeException: Cannot automatically determine case class field names and order for 'Test', please use the 'jsonFormat' overload with explicit field name specification and Caused by: java.lang.RuntimeException: Case class Test declares additional fields (Note I added a line with Test("a","b").toJson to your code)Greaseball
@Greaseball change to jsonFormat(Test.apply _, "param")Kumquat
Thanks, now it works, but I don't understand why exactly. Could you edit the answer with an explanation?Greaseball
B
12

I also experienced this exception, however in my case it was due to a 'val' definition within the case class (not as passed argument).

Problematic case class:

case class Color(name: String) {
  val code: Int = ...
}

Working case class:

case class Color(name: String) {
  def code: Int = ...
}

Which is kind of misfortunate as, now the 'code' will be computed on each call.

(Used Akka Http Version - 10.1.1)

Bogosian answered 5/9, 2018 at 9:48 Comment(0)
K
5

It should work perfectly, probably you have some ambiguity in implicits. This works perfectly:

import spray.json._, DefaultJsonProtocol._

case class Test(param: String*)
object Test {
  implicit val testFormat = jsonFormat1(Test.apply)
}

Like a best practice advice, don't use Protocol pattern, it leads to large implicit errors in big project, always define implicits in your companion object (of cause there are exception cases). Another point avoid inheritance, it's not really needed.

Scala * pattern is just a sugar for Seq type constructor, so it should find seqFormat (un)marshaller for this cases.

Update

It doesn't work because Spray uses ClassManifest to extract field names from the copy function, but the compiler doesn't generate this function for case classes with varargs in constructor:

case class Test(params: String*)
def man[A: ClassManifest] = implicitly[ClassManifest[A]]
man[Test].erasure.getDeclaredMethods.filter(_.getName.contains("copy"))
res4: Array[java.lang.reflect.Method] = Array()

scala> case class Test(param: String)
defined class Test

scala> man[Test].erasure.getDeclaredMethods.filter(_.getName.contains("copy"))
warning: there was one deprecation warning; re-run with -deprecation for details
res5: Array[java.lang.reflect.Method] = Array(public Test Test.copy(java.lang.String), public java.lang.String Test.copy$default$1())

So you need to provide the field name manually. BTW i didn't know this before

Kumquat answered 30/1, 2015 at 8:51 Comment(5)
Tried in the scala console: error: value apply is not a member of object Test \n Note: implicit value testFormat is not applicable here because it comes after the application point and it lacks an explicit result typeGreaseball
@Greaseball you need to switch to ":paste" mode then eval itKumquat
Now there is an error: java.lang.RuntimeException: Cannot automatically determine case class field names and order for 'Test', please use the 'jsonFormat' overload with explicit field name specification and Caused by: java.lang.RuntimeException: Case class Test declares additional fields (Note I added a line with Test("a","b").toJson to your code)Greaseball
@Greaseball change to jsonFormat(Test.apply _, "param")Kumquat
Thanks, now it works, but I don't understand why exactly. Could you edit the answer with an explanation?Greaseball
C
0

I suggest you to define your case class with name: List[String] and create companion object with custom apply function. You are still able to create objects like Colors("blue", "green").

case class Colors(name: List[String])

object Colors {
  def apply(name: String*): Colors = new Colors(name.toList)
  def apply(names: List[String]): Colors = new Colors(names)
}


Colors("blue", "green")
Catoptrics answered 30/1, 2015 at 8:39 Comment(1)
When I add implicit val colorsFormat = jsonFormat1(Colors.apply) then there is an error ambiguous reference to overloaded definition and when I use implicit val colorsFormat = jsonFormat1(Colors) then the error is type mismatch; found : Colors.type required: ? => ?Greaseball

© 2022 - 2024 — McMap. All rights reserved.