play-json icon indicating copy to clipboard operation
play-json copied to clipboard

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

Open bwbecker opened this issue 1 year ago • 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}")

bwbecker avatar Jan 23 '24 16:01 bwbecker

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}")

bwbecker avatar Jan 23 '24 16:01 bwbecker

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

ihostage avatar Jan 23 '24 17:01 ihostage

@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.

bwbecker avatar Jan 23 '24 20:01 bwbecker

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

mkurz avatar Jan 24 '24 21:01 mkurz

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.

cchantep avatar Jan 24 '24 21:01 cchantep