playframework / play-json

The Play JSON library
Apache License 2.0
361 stars 134 forks source link

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

Closed bwbecker closed 3 months ago

bwbecker commented 9 months ago

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}")
bwbecker commented 9 months ago

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 commented 9 months ago

@bwbecker Can it be related to this https://github.com/orgs/playframework/discussions/12292?

bwbecker commented 9 months ago

@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 9 months ago

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

cchantep commented 9 months ago

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.