spray-json failing for Seq of Eithers
Asked Answered
D

2

7

Not sure this is a bug, but the following demo fails on the final cases:

import spray.json._
import DefaultJsonProtocol._

object SprayTest {
  1.toJson
  "".toJson
  (Left(1): Either[Int, String]).toJson
  (Right(""): Either[Int, String]).toJson
  Seq(1).toJson
  Seq("").toJson
  Seq(Left(1), Right("")).toJson
  Seq(Left(1), Right("")).toJson(seqFormat(eitherFormat(IntJsonFormat, StringJsonFormat)))
}

So all the building blocks appear to work, but the composition of the format for Seq and Either fails, even if I try to spoon-feed it.

I see the following errors:

[error] SprayTest.scala:11: Cannot find JsonWriter or JsonFormat type class for Seq[Product with Serializable with scala.util.Either[Int,String]]
[error]   Seq(Left(1), Right("")).toJson
[error]                           ^
[error] SprayTest.scala:12: type mismatch;
[error]  found   : spray.json.DefaultJsonProtocol.JF[Either[Int,String]]
[error]     (which expands to)  spray.json.JsonFormat[Either[Int,String]]
[error]  required: spray.json.JsonFormat[Product with Serializable with scala.util.Either[Int,String]]
[error] Note: Either[Int,String] >: Product with Serializable with scala.util.Either[Int,String] (and spray.json.DefaultJsonProtocol.JF[Either[Int,String]] <: spray.json.JsonFormat[Either[Int,String]]), but trait JsonFormat is invariant in type T.
[error] You may wish to define T as -T instead. (SLS 4.5)
[error]   Seq(Left(1), Right("")).toJson(seqFormat(eitherFormat(IntJsonFormat, StringJsonFormat)))

Any idea what gives?

Dominickdominie answered 2/4, 2016 at 20:15 Comment(0)
H
14

This is one of the most annoying things about Either—the Left and Right constructors both extend Product and Serializable, but Either itself doesn't, which leads to awful inferred types:

scala> Seq(Left(1), Right(""))
res0: Seq[Product with Serializable with scala.util.Either[Int,String]] = List(Left(1), Right())

Because JsonFormat is invariant in its type parameter, the fact that you have an instance for A doesn't mean you have an instance for Product with Serializable with A. In your case specifically, there is actually an instance for Either[Int, String], but the extra garbage in the inferred type means the compiler can't find it.

A similar thing happens if you don't have a Right in the sequence:

scala> Seq(Left(1), Left(2)).toJson
<console>:18: error: Cannot find JsonWriter or JsonFormat type class for Seq[scala.util.Left[Int,Nothing]]
       Seq(Left(1), Left(2)).toJson
                             ^

You can fix both problems by providing a type instead of using the inferred one:

scala> val xs: Seq[Either[Int, String]] = Seq(Left(1), Right(""))
xs: Seq[Either[Int,String]] = List(Left(1), Right())

scala> xs.toJson
res1: spray.json.JsValue = [1,""]

In many cases this isn't an issue, since you'll often get your Either values from methods that explicitly return an Either instead of using Left and Right directly in ways that lead to this problem.

As a footnote: this is why you should always have your root sealed trait (or sealed class) extend Product with Serializable when you're defining your own ADTs. We'd all be a lot better off if the standard library designers had followed that advice.

Hylophagous answered 2/4, 2016 at 20:34 Comment(1)
This fixes my example here, but unfortunately, it doesn't fix my real code :( But very informative, thanks!Dominickdominie
P
2

I think if you add type ascriptions to the elements of the Seqs It'll compile:

Seq(Left(1): Either[Int, String], Right(""): Either[Int, String]).toJson
Seq(Left(1): Either[Int, String], Right(""): Either[Int, String]).toJson(seqFormat(eitherFormat(IntJsonFormat, StringJsonFormat))

you can also give it a single type ascription:

(Seq(Left(1), Right("")): Either[Int, String]).toJson
(Seq(Left(1), Right("")): Either[Int, String]).toJson(seqFormat(eitherFormat(IntJsonFormat, StringJsonFormat))

I think the problem is scalac's trying to determine the common least upper bounds between the elements you're providing to Seq to derive a single type (since standard collections require homogeneous data types for their elements) and it doesn't infer what you want it to without giving it help. If the scala standard library had added extends Product with Serializable to the abstract class Either definition you wouldn't need to do this, but since both the subtypes Right and Left are case classes (which implicitly do extend Product and Serializable), they get included in the inferred type, which is causing you issues with the invariant type required by spray.

Percy answered 2/4, 2016 at 20:44 Comment(1)
looks like Travis beat me to it. :-)Percy

© 2022 - 2024 — McMap. All rights reserved.