Hi, I have a case class where one of the fields is referencing another object of the same type.
When I try to decode[NestedClass](str), it throws an NPE. Same when I try to encode to Json.
I'm using circe version 0.7.0 with scala 2.12.
Here is an simple example:
import io.circe._
import io.circe.parser._
import io.circe.syntax._
object NestedExample {
case class NestedClass(id: Long, displayName: String, nesting: Option[NestedClass])
implicit val nestedClassDecoder: Decoder[NestedClass] =
Decoder.forProduct3("id", "display_name", "nesting")(NestedClass.apply)
implicit val nestedClassEncoder: Encoder[NestedClass] =
Encoder.forProduct3("id", "display_name", "nesting") { nc =>
(nc.id, nc.displayName, nc.nesting)
}
def main(args: Array[String]): Unit = {
val testString =
"""
|{
| "id": 10000,
| "display_name": "foobar",
| "nesting": {
| "id": 10001,
| "display_name": "quux"
| }
|}
""".stripMargin
val errorOrNesting = decode[NestedClass](testString) // NPE
val inner: NestedClass = NestedClass(10001L, "inner", None)
val outer: NestedClass = NestedClass(10000L, "outer", Some(inner))
println(outer.asJson.spaces2) // NPE
}
}
Stacktrace when decoding
Exception in thread "main" java.lang.NullPointerException
at io.circe.Decoder$.$anonfun$decodeOption$1(Decoder.scala:623)
at io.circe.Decoder$$anon$27.tryDecode(Decoder.scala:312)
at io.circe.ACursor.as(ACursor.scala:336)
at io.circe.ACursor.get(ACursor.scala:343)
at io.circe.ProductDecoders$$anon$3.apply(ProductDecoders.scala:36)
at io.circe.Decoder.decodeJson(Decoder.scala:48)
at io.circe.Decoder.decodeJson$(Decoder.scala:48)
at io.circe.ProductDecoders$$anon$3.decodeJson(ProductDecoders.scala:35)
at io.circe.Parser.finishDecode(Parser.scala:11)
at io.circe.Parser.finishDecode$(Parser.scala:8)
at io.circe.parser.package$.finishDecode(package.scala:5)
at io.circe.Parser.decode(Parser.scala:25)
at io.circe.Parser.decode$(Parser.scala:24)
at io.circe.parser.package$.decode(package.scala:5)
at com.marcoy.circe.nested.NestedExample$.main(NestedExample.scala:32)
at com.marcoy.circe.nested.NestedExample.main(NestedExample.scala)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
Stacktrace when encoding
Exception in thread "main" java.lang.NullPointerException
at io.circe.Encoder$$anon$28.apply(Encoder.scala:215)
at io.circe.Encoder$$anon$28.apply(Encoder.scala:213)
at io.circe.ProductEncoders$$anon$3.encodeObject(ProductEncoders.scala:38)
at io.circe.ObjectEncoder.apply(ObjectEncoder.scala:13)
at io.circe.ObjectEncoder.apply$(ObjectEncoder.scala:13)
at io.circe.ProductEncoders$$anon$3.apply(ProductEncoders.scala:35)
at io.circe.syntax.package$EncoderOps$.asJson$extension(package.scala:8)
at com.marcoy.circe.nested.NestedExample$.main(NestedExample.scala:36)
at com.marcoy.circe.nested.NestedExample.main(NestedExample.scala)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
Hmm, this is unfortunate. I'm afraid for right now the only option is to use the more explicit approach to defining these instances:
import cats.instances.either._
import cats.syntax.cartesian._
import io.circe._, io.circe.parser._, io.circe.syntax._
case class NestedClass(id: Long, displayName: String, nesting: Option[NestedClass])
object NestedClass {
implicit val nestedClassDecoder: Decoder[NestedClass] = Decoder.instance { c =>
(
c.get[Long]("id") |@|
c.get[String]("display_name") |@|
c.get[Option[NestedClass]]("nesting")
).map(NestedClass.apply)
}
implicit val nestedClassEncoder: Encoder[NestedClass] = Encoder.instance {
case NestedClass(i, d, n) => Json.obj(
"id" -> i.asJson,
"display_name" -> d.asJson,
"nesting" -> n.asJson
)
}
}
Or you could use generic-extras:
import io.circe.generic.extras._, io.circe.generic.extras.auto._
import io.circe.parser._, io.circe.syntax._
implicit val config: Configuration = Configuration.default.withSnakeCaseKeys
case class NestedClass(id: Long, displayName: String, nesting: Option[NestedClass])
And then:
scala> val inner: NestedClass = NestedClass(10001L, "inner", None)
inner: NestedClass = NestedClass(10001,inner,None)
scala> val outer: NestedClass = NestedClass(10000L, "outer", Some(inner))
outer: NestedClass = NestedClass(10000,outer,Some(NestedClass(10001,inner,None)))
scala> val json = outer.asJson.spaces2
json: String =
{
"id" : 10000,
"display_name" : "outer",
"nesting" : {
"id" : 10001,
"display_name" : "inner",
"nesting" : null
}
}
scala> decode[NestedClass](json)
res0: Either[io.circe.Error,NestedClass] = Right(NestedClass(10000,outer,Some(NestedClass(10001,inner,None))))
Neither of these approaches requires the same kind of recursive reference to the current encoders or decoders that you see with forProductN.
For what it's worth you get the same thing with the equivalent method in Argonaut:
import argonaut._, Argonaut._
case class NestedClass(id: Long, displayName: String, nesting: Option[NestedClass])
implicit val decodeNestedClass: DecodeJson[NestedClass] =
jdecode3L(NestedClass.apply)("id", "display_name", "nesting")
val testString =
"""
|{
| "id": 10000,
| "display_name": "foobar",
| "nesting": {
| "id": 10001,
| "display_name": "quux"
| }
|}
""".stripMargin
val result = Parse.decode[NestedClass](testString)
This was reported as an issue there a couple of years ago, but closed as "not an Argonaut issue per se". I'm not sure we'll be able to do better than that, but at the very least we'll get these methods documented so that it's clear they don't work for recursive case classes.
Thanks for the report, @marcoy, and apologies for the inconvenience.
No worries, the two alternatives you outlined are not that bad at all, Thanks! In fact, I didn't know about the .instance(...) and the configuration for snake case.
I'm closing this. Thank you for the help.
Do you mind reopening this, @marcoy? I'd like to have a reminder to take a look at it, and to add some documentation if we decide a fix isn't worth it. I can also open a new issue if you'd prefer.
Sure thing.
This is an issue when writing an Encoder[Throwable] because of getCause:
implicit val throwableEncoder: Encoder[Throwable] = Encoder.instance { throwable =>
import io.circe._, io.circe.syntax._
val message = Option(throwable.getMessage).getOrElse("")
val elements = Option(throwable.getStackTrace).map(_.toSeq).getOrElse(Seq.empty)
val declaringClass = throwable.getClass.getName
Json.obj(
"message" -> message.asJson,
"@class" -> declaringClass.asJson,
"elements" -> elements.asJson,
"cause" -> Option(throwable.getCause).asJson
)
}
implicit val stackTraceElementEncoder: Encoder[StackTraceElement] = Encoder.instance { element =>
import io.circe._, io.circe.syntax._
val filename = Option(element.getFileName)
val lineNumber = element.getLineNumber
val methodName = element.getMethodName
val className = element.getClassName
Json.obj(
"fileName" -> filename.asJson,
"lineNumber" -> lineNumber.asJson,
"methodName" -> methodName.asJson,
"className" -> className.asJson
)
}
Nope, there's something odder going on. Any encoder that uses Throwable or a subclass there of will give the same exception:
implicit val healthCheckResultEncoder: Encoder[HealthCheck.Result] =
Encoder.forProduct5("status", "message", "tags", "throwable", "creationTime") { u =>
val status = u.status match {
case HealthCheck.Status.Healthy => "healthy"
case HealthCheck.Status.Unhealthy => "unhealthy"
case HealthCheck.Status.Maintenance => "maintenance"
case HealthCheck.Status.Zombie => "zombie"
}
val message = u.message
val tags = u.tags
val throwable: Option[IllegalArgumentException] = Some(new IllegalArgumentException)
val creationTime = u.creationTime
(status, message, tags, throwable, creationTime)
}
implicit val throwableEncoder: Encoder[IllegalArgumentException] = Encoder.instance { throwable =>
import io.circe._, io.circe.syntax._
val message = "derp" // Option(throwable.getMessage)
Json.obj(
"message" -> message.asJson
)
}
results in:
[info] java.lang.NullPointerException: null
[info] at io.circe.Encoder$$anon$28.apply(Encoder.scala:219)
[info] at io.circe.Encoder$$anon$28.apply(Encoder.scala:217)
[info] at io.circe.ProductEncoders$$anon$5.encodeObject(ProductEncoders.scala:62)
[info] at io.circe.ObjectEncoder.apply(ObjectEncoder.scala:13)
[info] at io.circe.ObjectEncoder.apply$(ObjectEncoder.scala:13)
[info] at io.circe.ProductEncoders$$anon$5.apply(ProductEncoders.scala:59)
[info] at io.circe.Encoder$$anon$7.$anonfun$encodeObject$1(Encoder.scala:290)
[info] at scala.collection.TraversableLike.$anonfun$map$1(TraversableLike.scala:234)
[info] at scala.collection.immutable.Map$Map1.foreach(Map.scala:120)
[info] at scala.collection.TraversableLike.map(TraversableLike.scala:234)
@wsargent That shouldn't fail鈥攜ou're getting NPEs from that code specifically?
If I comment out the throwable, it works fine. Add it back in, and boom.
Which throwable?
Well, the original throwable is
HealthCheckService(system).register("database", HealthCheck {
//HealthCheck.Result.healthy("Database connection is successful!")
throw new IllegalStateException("BLLALAGH")
})
and when I don't include it in the encoding then everything works:
implicit val healthCheckResultEncoder: Encoder[HealthCheck.Result] =
Encoder.forProduct4("status", "message", "tags", "creationTime") { u =>
val status = u.status match {
case HealthCheck.Status.Healthy => "healthy"
case HealthCheck.Status.Unhealthy => "unhealthy"
case HealthCheck.Status.Maintenance => "maintenance"
case HealthCheck.Status.Zombie => "zombie"
}
val message = u.message
val tags = u.tags
val creationTime = u.creationTime
(status, message, tags, creationTime)
}
I am putting together a test project to better isolate this.
Can't replicate in a subproject: it won't even compile:
import io.circe._, io.circe.syntax._
object Main {
def main(args: Array[String]): Unit = {
implicit val throwableEncoder: Encoder[Throwable] = Encoder.instance { throwable =>
val maybeMessage = Option(throwable.getMessage)
val maybeThrowable = Option(throwable.getCause)
Json.obj(
"message" -> maybeMessage.asJson,
"cause" -> maybeThrowable.asJson
)
}
try {
throw new IllegalStateException("whoops")
} catch {
case ex: Throwable =>
val json = ex.asJson
println(json.noSpaces)
}
}
}
Error:(12, 35) forward reference extends over definition of value throwableEncoder
"cause" -> maybeThrowable.asJson
The subtyping / variance is off as well -- if I want to work with Exception, I have to do this:
import io.circe._, io.circe.syntax._
object Main {
def main(args: Array[String]): Unit = {
implicit val throwableEncoder: Encoder[Exception] = Encoder.instance { throwable =>
val maybeMessage = Option(throwable.getMessage)
val maybeThrowable = Option(throwable.getCause)
Json.obj(
"message" -> maybeMessage.asJson,
//"cause" -> maybeThrowable.asJson
)
}
try {
throw new IllegalStateException("whoops")
} catch {
case ex: Exception =>
val json = ex.asJson
println(json.noSpaces)
}
}
}
Okay, it looks like the issue is related to defining the implicit for Encoder[Throwable] after the healthcheckResultEncoder. It works fine when I do "stacktraceEncoder / throwableEncoder / healthcheckEncoder". Still very strange though.
I also got bit by this, getting a
scala>
java.lang.NullPointerException
at io.circe.SeqDecoder.apply(SeqDecoder.scala:18)
at io.circe.Decoder$$anon$16.apply(Decoder.scala:111)
at io.circe.Decoder$class.tryDecode(Decoder.scala:33)
at io.circe.Decoder$$anon$16.tryDecode(Decoder.scala:110)
at io.circe.ACursor.as(ACursor.scala:336)
at io.circe.ACursor.get(ACursor.scala:343)
at io.circe.ProductDecoders$$anon$1.apply(ProductDecoders.scala:12)
at io.circe.Json.as(Json.scala:97)
The solution (of making an explicit decoder) works for me as well, instead of usingDecoder.forProduct
Most helpful comment
Hmm, this is unfortunate. I'm afraid for right now the only option is to use the more explicit approach to defining these instances:
Or you could use generic-extras:
And then:
Neither of these approaches requires the same kind of recursive reference to the current encoders or decoders that you see with
forProductN.For what it's worth you get the same thing with the equivalent method in Argonaut:
This was reported as an issue there a couple of years ago, but closed as "not an Argonaut issue per se". I'm not sure we'll be able to do better than that, but at the very least we'll get these methods documented so that it's clear they don't work for recursive case classes.
Thanks for the report, @marcoy, and apologies for the inconvenience.