Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JsonCodec for sealed traits requires an explicit object definition #251

Closed
travisbrown opened this issue Apr 3, 2016 · 21 comments
Closed

Comments

@travisbrown
Copy link
Member

As reported by @ngbinh, the following does not compile in circe 0.4.0-RC1:

import io.circe.generic.JsonCodec

@JsonCodec sealed trait A
case class B(b: String) extends A
case class C(c: Int) extends A

The expanded version fails as well:

import io.circe.{ Decoder, Encoder }

sealed trait A

object A {
  implicit val encodeA: Encoder[A] = io.circe.generic.semiauto.deriveEncoder[A]
  implicit val decodeA: Decoder[A] = io.circe.generic.semiauto.deriveDecoder[A]
}

case class B(b: String) extends A
case class C(c: Int) extends A

But can be fixed by moving the object A definition after the case class definitions.

Similarly, it's possible to work around the issue with @JsonCodec by adding an object definition (potentially empty) after the case classes:

import io.circe.generic.JsonCodec

@JsonCodec sealed trait A
case class B(b: String) extends A
case class C(c: Int) extends A

object A

This isn't that terrible, but it's an annoying thing to have to remember. I'm not sure we can fix the JsonCodec macro annotation so that this workaround isn't necessary, but we should at least take a look (probably after the 0.4.0 release).

@ngbinh
Copy link
Contributor

ngbinh commented Apr 3, 2016

The work around doesn't seem to work (both expanded or @JsonCodec ones.)
everything works fine in 0.3.0 though, so probably something in 0.4.0-RC1 triggers that
The error message is

Could not find Lazy implicit value of type io.circe.generic.encoding.DerivedObjectEncoder[A]

and I only see problem with Encoder, Decoder doesn't seem to have any problem compiling

@travisbrown
Copy link
Member Author

@ngbinh Any chance you could share a reproduction? At least in the simple case above the workaround works just fine.

@ngbinh
Copy link
Contributor

ngbinh commented Apr 3, 2016

I will work on a reproduction. It could be because my B and C require other encoders as well so the order is still messed up.

@ngbinh
Copy link
Contributor

ngbinh commented Apr 3, 2016

it's funny though

sealed trait A

case class B(b: String) extends A
case class C(c: Int) extends A

object A {
  implicit val encodeA: Encoder[A] = io.circe.generic.semiauto.deriveEncoder[A]
  implicit val decodeA: Decoder[A] = io.circe.generic.semiauto.deriveDecoder[A]
}

gives me

/path-to-files.scala:18: could not find Lazy implicit value of type io.circe.generic.encoding.DerivedObjectEncoder[package.A]
[error]   implicit val encodeA: Encoder[A] = io.circe.generic.semiauto.deriveEncoder[A]

when I copy and paste the code in one of the class I have problem with.

@travisbrown
Copy link
Member Author

That's extremely weird. I'd love to see a minimization that triggers this—thanks for working on it.

@ngbinh
Copy link
Contributor

ngbinh commented Apr 15, 2016

So, I try to work on a small reproduce and this is what i found: https://github.com/ngbinh/scala-js-example-app/blob/circe-251/src/main/scala/example/Model.scala

import io.circe.generic.JsonCodec
import io.circe.syntax._

@JsonCodec sealed trait A
case class B(b: String) extends A
case class C(c: Int) extends A

object A

object Test {
  B("abc").asJson.noSpaces // -> error
}

Without B("abc").asJson.noSpaces, it compiles but with that in, I see

could not find implicit value for parameter encoder: io.circe.Encoder[example.B]
[error]   B("abc").asJson.noSpaces
[error]            ^
[error] one error found

@ngbinh
Copy link
Contributor

ngbinh commented Apr 15, 2016

And of course if I ask for

object Test {
  implicitly[Encoder[B]
}

then

could not find implicit value for parameter e: io.circe.Encoder[example.B]
[error]   implicitly[Encoder[B]]
[error]             ^
[error] one error found

@ngbinh
Copy link
Contributor

ngbinh commented Apr 15, 2016

and we are on circe 0.4.1. Also, auto works as expected

@travisbrown
Copy link
Member Author

Oh, now I think I see the problem.

When you ask semiauto for an instance for the root of a sealed trait hierarchy, it will find or derive its own instances for the case classes in the hierarchy, but if it derives them, it doesn't put them into implicit scope or anything like that. You've asked for a Decoder[A] instance, and that's what you get—not one for B or C. And Decoder is invariant, so you can't call asJson on a value that's statically typed as B—you have to upcast:

scala>  B("abc").asJson.noSpaces
<console>:24: error: could not find implicit value for parameter encoder: io.circe.Encoder[B]
        B("abc").asJson.noSpaces
                 ^

scala> (B("abc"): A).asJson.noSpaces
res1: String = {"B":{"b":"abc"}}

So you could either upcast or explicitly derive instances for all of the children—which you can do with @JsonCodec:

@JsonCodec sealed trait A
@JsonCodec case class B(b: String) extends A
@JsonCodec case class C(c: Int) extends A
object A

And then:

scala> B("abc").asJson.noSpaces
res2: String = {"b":"abc"}

Does that make sense?

@ngbinh
Copy link
Contributor

ngbinh commented Apr 15, 2016

ok, makes sense now. The trouble now is when constructing a model M, we usually ask for implicit val encoder: Encoder[M].

If we add @JsonCodec for the children, would a decoder for B be able to decode (B("abc"): A).asJson.noSpaces or do we have to use the one from A?

@travisbrown
Copy link
Member Author

The exact static type always determines which instance will be used, and circe-generic will always give different instances for A and B here.

If you wanted instances for A that didn't use the object wrapper, you could do something this:

import io.circe._, io.circe.generic.JsonCodec, io.circe.syntax._

sealed trait A
@JsonCodec case class B(b: String) extends A
@JsonCodec case class C(c: Int) extends A
object A {
  implicit val decodeA: Decoder[A] = Decoder[B].map[A](identity).or(Decoder[C].map[A](identity))
  implicit val encodeA: Encoder[A] = Encoder.instance {
    case b @ B(_) => b.asJson
    case c @ C(_) => c.asJson
  }
}

And then:

scala> (B("abc"): A).asJson.noSpaces
res0: String = {"b":"abc"}

scala> B("abc").asJson.noSpaces
res1: String = {"b":"abc"}

This isn't the default because in some cases it can lead to ambiguity in decoding, but if your case class member names don't overlap it can be a reasonable thing to do.

@ngbinh
Copy link
Contributor

ngbinh commented Apr 15, 2016

thanks! Got it now.

@non
Copy link
Contributor

non commented Jun 14, 2016

Is this issue blocking Circe 0.5.0?

@dircsem
Copy link

dircsem commented Apr 7, 2017

I get the same error when trying to convert a simple case class to Json

case class Dirceu(id:Int, str:Option[String] = None)
Dirceu(1).asJson
<console>:48: error: could not find implicit value for parameter encoder: io.circe.Encoder[Dirceu]
       Dirceu(1).asJson

And if I try to use the annotation the error is:

@io.circe.generic.JsonCodec case class Dirceu(id:Int, str:Option[String] = None)
<console>:11: error: macro annotation could not be expanded (the most common reason for that is that you need to enable the macro paradise plugin; another possibility is that you try to use macro annotation in the same compilation run that defines it)
       @io.circe.generic.JsonCodec case class Dirceu(id:Int, str:Option[String] = None)

@fosskers
Copy link

fosskers commented Apr 12, 2017

Needing to care about the order of where the sealed trait's companion object is in the file is still an issue with 0.7.0. I'm using semiauto derivation without @JsonCodec.

@ssabnis
Copy link

ssabnis commented May 23, 2017

JSonCodec fails when the last case class has nested case classes in it.

could not find implicit value for parameter encoder:

@nafg
Copy link

nafg commented Sep 8, 2017

Is it supposed to work if I nest the case classes inside the sealed trait's companion?

@JsonCodec
sealed trait A

object A {
  case class A1(x: String) extends A
  case class A2(y: Int) extends A
}

@travisbrown travisbrown removed this from the 0.5.0 milestone Dec 8, 2017
@bbarker
Copy link

bbarker commented Apr 27, 2018

I'm trying to use @JsonCodec as described here, but keep running into macro annotation could not be expanded.

Relevant code snippet:

@JsonCodec
sealed trait Command {
  val id: CommandId
  val body: String
  type M <: CommandMetaData
  val meta: M
}

// Value classes don't work with autowire
case class JobRelPath(value: String) // extends AnyVal

//sealed trait FileCommand extends Command {
//  val fileContents: Map[JobRelPath, String]
//}

final case class OneShot(id: CommandId, body: String, meta: SysCmdMetaData) extends Command {
  override type M = SysCmdMetaData
}

final case class Repl(id: CommandId, body: String, meta: SysCmdMetaData) extends Command{
  override type M = SysCmdMetaData
}

final case class CommandInRepl(id: CommandId, body: String, meta: ReplCmdMetaData) extends Command{
  override type M = ReplCmdMetaData
}

final case class ExecFile(
  id: CommandId,
  body: String,
  meta: SysCmdMetaData,
  fileContents: Map[JobRelPath, String]
) extends Command {
  override type M = SysCmdMetaData
}

object Command

Relevant snippets of my build.sbt:

val scala211 = "2.11.8"
val scala212 = "2.12.4"
val scalaVersionSelect = scala212

val akkaHttpDep = "com.typesafe.akka" %% "akka-http" % "10.0.9"
val ammoniteDep = "com.lihaoyi" %% "ammonite-ops" % "1.0.1"

val scalatest = Def.setting(
  "org.scalatest" %%% "scalatest" % "3.2.0-SNAP7" % "test")

val cats = Def.setting(
  "org.typelevel" %%% "cats" % "0.9.0"
)

val autowireDeps = Def.setting(Seq(   
  "com.lihaoyi" %%% "autowire" % "0.2.6",
  "io.suzaku" %%% "boopickle" % "1.2.6"))

val mhtmlDeps = Def.setting(Seq(
  "in.nvilla" %%% "monadic-html" % "0.4.0-RC1",
  "in.nvilla" %%% "monadic-rx-cats" % "0.4.0-RC1",
  "org.scala-js" %%% "scalajs-dom" % "0.9.2"
))

val circeVersion = "0.10.0-M1"
val circeDeps = Def.setting(Seq(
  "io.circe" %% "circe-core",
  "io.circe" %% "circe-generic"
//  "io.circe" %% "circe-parser" // Don't need so far
).map(_ % circeVersion))

val commonSettings = Seq(
  version := "1.0-SNAPSHOT",
  scalaVersion := scalaVersionSelect,
  scalacOptions := Seq(
    "-encoding", "UTF-8",
    "-feature",
    "-unchecked",
    "-deprecation:false",
    "-Xfatal-warnings",
    //"-Xlint",
    "-Xlint:-unused,_",
    // "-Ywarn-unused:imports", // Disabling during normal dev - too annoying!
    "-Yno-adapted-args",
    "-Ywarn-numeric-widen",
    "-Ywarn-value-discard",
    "-Xfuture"),
  resolvers ++= Seq(
    Resolver.sonatypeRepo("public")
    ,"amateras-repo" at "http://amateras.sourceforge.jp/mvn/" // For ace editor facade
    //,"sonatype-staging" at "https://oss.sonatype.org/content/repositories/staging/"
  ),
  testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-oDF"),
  // Shared /config between all projects
  unmanagedClasspath in Compile <+= (baseDirectory) map { bd => Attributed.blank(bd / ".." / "config") },
  unmanagedClasspath in Runtime <++= (unmanagedClasspath in Compile),
  unmanagedClasspath in Test <++= (unmanagedClasspath in Compile),
  libraryDependencies ++= Seq(
    "org.typelevel"  %%% "squants"  % "1.3.0"
  ) ++ circeDeps.value
) // ++ warnUnusedImport // Disabling during normal dev - too annoying!

autoCompilerPlugins := true

addCompilerPlugin( // For circe generic:
  "org.scalamacros" % s"paradise_$scalaVersionSelect" % "2.1.1" /*cross CrossVersion.full*/
)
val settingsJVM = commonSettings

//....

If it helps, I can try to reproduce this in a public repo.

@bbarker
Copy link

bbarker commented Apr 27, 2018

I don't think it is necessary with the addCompilerPlugin call (also I think the following may be for the scalameta based plugin), but if I add "-Xplugin-require:macroparadise", I get Missing required plugin: macroparadise

@cartazio
Copy link

question: for the sealed trait A
and the case class B , case class C,
would putting the implicits in object B and object C respectively also work?

julienrf pushed a commit to scalacenter/circe that referenced this issue May 13, 2021
@hamnis
Copy link
Collaborator

hamnis commented Jul 27, 2023

Closing in cleanup run, If anyone cares about this, please comment and I'll reopen.

@hamnis hamnis closed this as not planned Won't fix, can't repro, duplicate, stale Jul 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants