playframework/play-json

Difference between scala 3.3.1 and 2.13.12 when case class constructor is overridden with apply

bwbecker opened this issue · 5 comments

Play JSON Version (2.5.x / etc)

2.10.4

API (Scala / Java / Neither / Both)

Scala

Operating System (Ubuntu 15.10 / MacOS 10.10 / Windows 10)

MacOS Sonoma 14.2.1

bwbecker@beta caseApply % uname -a
Darwin beta 23.2.0 Darwin Kernel Version 23.2.0: Wed Nov 15 21:53:18 PST 2023; root:xnu-10002.61.3~2/RELEASE_ARM64_T6000 arm64

JDK (Oracle 1.8.0_72, OpenJDK 1.8.x, Azul Zing)

bwbecker@beta caseApply % java -version
openjdk version "11.0.19" 2023-04-18
OpenJDK Runtime Environment Homebrew (build 11.0.19+0)
OpenJDK 64-Bit Server VM Homebrew (build 11.0.19+0, mixed mode)

Library Dependencies

None

Expected Behavior

Please describe the expected behavior of the issue, starting from the first action.

I expect playJson using scala 3.3.1 to call the apply method in the case classes' companion object, just like it did using Scala 2.13.12

Actual Behavior

With Scala 2.13.12 playJson calls A.apply; with Scala3.3.1 it does not.

Using scala 2.13.12:

bwbecker@beta caseApply % scala-cli --scala 2.13.12 caseApply.sc
Compiling project (Scala 2.13.12, JVM (11))
Compiled project (Scala 2.13.12, JVM (11))
Instantiate A without playJson
A.apply
A.constructor
a = A(LOWERCASE)

Instantiate A using playJson
A.apply                                            // A.apply is called; it calls the constructor
A.constructor
b=A(JSON)

Using scala 3.3.1:

bwbecker@beta caseApply % scala-cli --scala 3.3.1 caseApply.sc
Compiling project (Scala 3.3.1, JVM (11))
Compiled project (Scala 3.3.1, JVM (11))
Instantiate A without playJson
A.apply
A.constructor
a = A(LOWERCASE)

Instantiate A using playJson
A.constructor                           // constructor called directly; apply is not
b=A(json)
bwbecker@beta caseApply % 

Reproducible Test Case

In the file caseApply.sc:

//> using dep     com.typesafe.play::play-json:2.10.4

import play.api.libs.json._

case class A(a:String) {
    println("A.constructor")
}

object A {
    def apply(a:String):A = {
        println("A.apply")
        new A(a.toUpperCase())
    }
}


implicit val fmtA:Format[A] = Json.format[A]

println("Instantiate A without playJson")
val a = A("lowercase")
println(s"a = ${a}")

println("\nInstantiate A using playJson")
val jsString = """{"a":"json"}"""

val b = Json.parse(jsString).as[A]
println(s"b=${b}")

I modified the above example to use a regular class rather than a case class by adding an unapply method. The code works with Scala 2.13.12 but with Scala 3.3.1 I get the following compile error:

bwbecker@beta caseApply % scala-cli --scala 3.3.1 nonCaseApply.sc
Compiling project (Scala 3.3.1, JVM (11))
[error] ./nonCaseApply.sc:23:31
[error] Instance not found: 'Conversion[nonCaseApply_.A, _ <: Product]'
[error] implicit val fmtA:Format[A] = Json.format[A]
[error]                               ^^^^^^^^^^^^^^
Error compiling project (Scala 3.3.1, JVM (11))
Compilation failed
bwbecker@beta caseApply % 

Here's the modified code:

//> using dep     com.typesafe.play::play-json:2.10.4

import play.api.libs.json._

class A(val a:String) {
    println("A.constructor")
    override def toString:String = a
}

object A {
    def apply(a:String):A = {
        println("A.apply")
        new A(a.toUpperCase())
    }

    def unapply(t:A):Option[(String)] = {
        println("A.unapply")
        Some((t.a))
    }
}


implicit val fmtA:Format[A] = Json.format[A]

println("Instantiate A without playJson")
val a = A("lowercase")
println(s"a = ${a}")

println("\nInstantiate A using playJson")
val jsString = """{"a":"json"}"""

val b = Json.parse(jsString).as[A]
println(s"b=${b}")

@ihostage, thanks for the reply. That might be part of the underlying cause. I'm not familiar with the playJson codebase, so I don't know.

A work-around is to define a Reads myself, similar to the one at https://www.playframework.com/documentation/2.8.x/ScalaJsonCombinators#Complex-Reads

Luckily, I have only one where this is causing me a problem. If I had many, it would be painful.

mkurz commented

@cchantep or also @ramazanyich what do you think about this?

apply/unapply are not used in Scala 3 derivation/macros.
The class must either extend Product (as any case class), or Conversion must be available.
Documentation can be updated.