0

Say I define the following case class:

case class C(i: Int) {
    lazy val incremented = copy(i = i + 1)
}

And then try to serialize it to json:

val mapper = new ObjectMapper()
mapper.registerModule(DefaultScalaModule)
val out = new StringWriter
mapper.writeValue(out, C(4))
val json = out.toString()
println("Json is: " + json)

It will throw the following exception:

Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]-
...

I don't know why is it trying to serialize the lazy val by default in the first place? This does not seem to me as the logical approach

And can I disable this feature?

user79074
  • 4,937
  • 5
  • 29
  • 57
  • I am using it in akka so if I can integrate with akka could very well be worth a try – user79074 Nov 18 '20 at 13:05
  • Having said that this is not an issue when using Json4s with Jackson. So a better option might be to use that as the custom serializer in akka if there is no way to disable serialization of lazy vals. – user79074 Nov 18 '20 at 13:26
  • 2
    You can try the `@transient` annotation. – pedrofurla Nov 18 '20 at 13:26
  • 1
    I would just recommend you to use a real **Scala** library instead of a **Java** one. For example, **circe** supports this out of the box, see [this](https://scastie.scala-lang.org/BalmungSan/zQo58wFOQPKBPQ2ef295AA/1). – Luis Miguel Mejía Suárez Nov 18 '20 at 14:18
  • I would recommend separating application logic from serialisation logic. – Tim Nov 18 '20 at 18:16

3 Answers3

2

This happens because Jackson is designed for Java. Specifically, note that:

  • Java has no idea of a lazy val
  • Java's normal semantics around fields and constructors don't allow the partitioning of fields into "needed for construction" and "derived for construction" (neither of those is a technical term) that Scala's combination of val in default constructor (implicitly present in a case class) and val in a class's body provide

The consequence of the second is that (except for beans, sometimes), Java-oriented serialization approaches tend to assume that anything which is a field (including private fields, since Java idiom is to make fields private by default) in the object needs to be serialized, with the ability to opt out through @transient annotations.

The first, in turn, means that lazy vals are implemented by the compiler in a way that includes a private field.

Thus to a Java-oriented serializer like Jackson, a lazy val without a @transient annotation gets serialized.

Scala-oriented serialization approaches (e.g. circe, play-json, etc.) tend to serialize case classes by only serializing the constructor parameters.

Levi Ramsey
  • 18,884
  • 1
  • 16
  • 30
  • As a side-note, having a `lazy val` cache a transformation is likely to be more pain than gain down the road, since as long as the source object is live, the transformed object will be live. Since you're using Akka and clustering, the extra memory consumption from doing this is very likely (eventual probability approaches 100%) to lead to GC pressure which will tear your cluster apart. – Levi Ramsey Nov 18 '20 at 19:18
  • If you want to cache the results of expensive transformations, it's almost surely better to use the `sealed abstract case class` trick to let you override the `apply` method to save created instances in a cache (you would then also have to provide a reimplementation of `copy`). – Levi Ramsey Nov 18 '20 at 19:23
  • Another alternative would be to subtly reimplement `lazy val` by combining with weak references. But it needs to be said, `lazy val` as a performance optimization is not only not free, it's sufficiently expensive that it's often not a net gain. – Levi Ramsey Nov 18 '20 at 19:25
0

The solution I found was to use json4s for my serialization rather than jackson databind. My issue arose using akka cluster so I had to add a custom serlializer to my project. For reference here is my complete implementation:

class Json4sSerializer(system: ExtendedActorSystem) extends Serializer {

private val actorRefResolver = ActorRefResolver(system.toTyped)

object ActorRefSerializer extends CustomSerializer[ActorRef[_]](format => (
    {
        case JString(str) =>
            actorRefResolver.resolveActorRef[AnyRef](str)
    },
    {
        case actorRef: ActorRef[_] =>
            JString(actorRefResolver.toSerializationFormat(actorRef))
    }
))

implicit private val formats = DefaultFormats + ActorRefSerializer

def includeManifest: Boolean = true
def identifier = 1234567

def toBinary(obj: AnyRef): Array[Byte] = {
    write(obj).getBytes(StandardCharsets.UTF_8)
}

def fromBinary(bytes: Array[Byte], clazz: Option[Class[_]]): AnyRef = clazz match {
    case Some(cls) =>
        read[AnyRef](new String(bytes, StandardCharsets.UTF_8))(formats, ManifestFactory.classType(cls))
    case None =>
        throw new RuntimeException("Specified includeManifest but it was never passed")
}
}
user79074
  • 4,937
  • 5
  • 29
  • 57
-3

You can't serialize that class because the value is infinitely recursive (hence the stack overflow). Specifically, the value of incremented for C(4) is an instance of C(5). The value of incremented for C(5) is C(6). The value of incremented for C(6) is C(7) and so on...

Since an instance of C(n) contains an instance of C(n+1) it can never be fully serlialized.

If you don't want a field to appear in the JSON, make it a function:

case class C(i: Int) {
    def incremented = copy(i = i + 1)
}

The root of this problem is trying to serialise a class that also implements application logic, which breaches the principle of Separation of Concerns (The S in SOLID).

It is better to have distinct classes for serialisation and populate them from the application data as necessary. This allows different forms of serialisation to be used without having to change the application logic.

Tim
  • 26,753
  • 2
  • 16
  • 29
  • 3
    I understand why there's an overflow. I just don't see why its serliazing lazy vals. There is no point. It defeats the whole purpose of lazy values and by converting to a function I lose the whole benefit in my code. My question is can I disable the serialization of lazy vals? – user79074 Nov 18 '20 at 13:04
  • 1
    @user79074 It doesn't matter that it is serialising lazy vals, what matters is that you are serialising application logic when you should only serialise pure data objects. Stop trying to serialise code and the problem will go away. – Tim Nov 25 '20 at 15:00