Skip to content

Change the API to be compatible with the upcoming Scala-3 v… #665

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions SCALA-3-MIGRATION-GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Scala 3 migration guide

### How to cross-compile

If you want to compile the same codebase with both scala-2 and scala-3,
you need to port the following methods.

```scala
// JSON derivation
// Why?: These methods are internal and shouldn't be exposed
jsonProduct(CaseClass.apply _) -> deriveJSON
toJsonProduct(CaseClass.apply _) -> deriveToJSON
toJsonProduct0(CaseObject) -> deriveToJSON
fromJsonProduct(CaseClass.apply _) -> deriveFromJSON
fromJsonProduct(CaseObject) -> deriveFromJSON

// deriveJSON dropping Enumeration support
// Why?: There's no derivation required for Enumeration
deriveJSON[SomeEnumeration.Value] -> jsonEnum(Enumeration)

// MongoFormat derivation
// Why?: These methods are internal and shouldn't be exposed
mongoProduct(CaseClass.apply _) -> deriveMongoFormat
mongoProduct0(CaseObject) -> deriveMongoFormat

// TypeSelectorContainer removed
// Why?: TypeSelectorContainer is internal and shouldn't be exposed.
json.asInstanceOf[TypeSelectorContainer].typeSelectors.map(_.typeValue)
->
json.subTypeNames

```

### Deprecated methods

The following methods will not be continued and no alternatives will be provided (because they are not used in our
codebase):

```scala
toJsonSingletonEnumSwitch
fromJsonSingletonEnumSwitch
```


7 changes: 6 additions & 1 deletion json/json-core/src/main/scala/io/sphere/json/JSON.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import org.json4s.JsonAST.JValue
import scala.annotation.implicitNotFound

@implicitNotFound("Could not find an instance of JSON for ${A}")
trait JSON[A] extends FromJSON[A] with ToJSON[A]
trait JSON[A] extends FromJSON[A] with ToJSON[A] {

// This field is only used in case we derive a trait, for classes/objects it remains empty
// It uses the JSON names not the Scala names (if there's @JSONTypeHint renaming a class the renamed name is used here)
def subTypeNames: List[String] = Nil
}

object JSON extends JSONInstances with JSONLowPriorityImplicits {
@inline def apply[A](implicit instance: JSON[A]): JSON[A] = instance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,72 @@ private[generic] object JSONMacros {
}
}

def generateJsonProduct(
c: blackbox.Context)(tpe: c.universe.Type, classSym: c.universe.ClassSymbol)(
classFnName: String,
objectFnName: String): c.universe.Tree = {
import c.universe._

if (classSym.isCaseClass && !classSym.isModuleClass) {
val classSymType = classSym.toType
val argList = classSymType.member(termNames.CONSTRUCTOR).asMethod.paramLists.head
val modifiers = Modifiers(Flag.PARAM)
val (argDefs, args) = (for ((a, i) <- argList.zipWithIndex) yield {
val argType = classSymType.member(a.name).typeSignatureIn(tpe)
val termName = TermName("x" + i)
val argTree = ValDef(modifiers, termName, TypeTree(argType), EmptyTree)
(argTree, Ident(termName))
}).unzip

val applyBlock = Block(
Nil,
Function(
argDefs,
Apply(Select(Ident(classSym.companion), TermName("apply")), args)
))
Apply(
Select(
reify(io.sphere.json.generic.`package`).tree,
TermName(classFnName)
),
applyBlock :: Nil
)
} else if (classSym.isCaseClass && classSym.isModuleClass) {
Apply(
Select(
reify(io.sphere.json.generic.`package`).tree,
TermName(objectFnName)
),
Ident(classSym.name.toTermName) :: Nil
)
} else c.abort(c.enclosingPosition, "Not a case class or (case) object")
}
def deriveToJSON_impl[A: c.WeakTypeTag](c: blackbox.Context): c.Expr[ToJSON[A]] = {
import c.universe._

val tpe = weakTypeOf[A]
val symbol = tpe.typeSymbol

if (symbol.isClass && (symbol.asClass.isCaseClass || symbol.asClass.isModuleClass))
c.Expr[ToJSON[A]](
generateJsonProduct(c)(tpe, symbol.asClass)("toJsonProduct", "toJsonProduct0"))
else
c.abort(c.enclosingPosition, "Not a case class or (case) object")
}

def deriveFromJSON_impl[A: c.WeakTypeTag](c: blackbox.Context): c.Expr[FromJSON[A]] = {
import c.universe._

val tpe = weakTypeOf[A]
val symbol = tpe.typeSymbol

if (symbol.isClass && (symbol.asClass.isCaseClass || symbol.asClass.isModuleClass))
c.Expr[FromJSON[A]](
generateJsonProduct(c)(tpe, symbol.asClass)("fromJsonProduct", "fromJsonProduct0"))
else
c.abort(c.enclosingPosition, "Not a case class or (case) object")
}

def deriveJSON_impl[A: c.WeakTypeTag](c: blackbox.Context): c.Expr[JSON[A]] = {
import c.universe._

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ package object generic extends Logging {

def deriveJSON[A]: JSON[A] = macro JSONMacros.deriveJSON_impl[A]
def deriveSingletonJSON[A]: JSON[A] = macro JSONMacros.deriveSingletonJSON_impl[A]
def deriveToJSON[A]: ToJSON[A] = macro JSONMacros.deriveToJSON_impl[A]
def deriveFromJSON[A]: FromJSON[A] = macro JSONMacros.deriveFromJSON_impl[A]


private def JSONofToAndFrom[A](toJSON: ToJSON[A], fromJSON: FromJSON[A]): JSON[A] = {
new JSON[A] {
Expand Down Expand Up @@ -407,6 +410,8 @@ package object generic extends Logging {
new JSON[T] with TypeSelectorContainer {
override def typeSelectors: List[TypeSelector[_]] = allSelectors

override def subTypeNames: List[String] = allSelectors.map(_.typeValue)

def read(jval: JValue): ValidatedNel[JSONError, T] = fromJSON.read(jval)

def write(t: T): JValue = toJSON.write(t)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package io.sphere.json

import java.util.UUID
import org.scalacheck.{Arbitrary, Gen}
import org.scalatest.matchers.must.Matchers
import org.scalatest.wordspec.AnyWordSpec
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks

import io.sphere.json.generic.deriveJSON
import io.sphere.json.generic.jsonProduct

class DeriveJSONCompatibilitySpec extends AnyWordSpec with Matchers with ScalaCheckPropertyChecks {

"jsonProduct must work the same as deriveJSON for case classes" in {

import DeriveJSONCompatibilitySpec._

val deriveSyntax: JSON[ClassOfManyTypes] = {
implicit val nestedF: JSON[NestedClass] = deriveJSON
deriveJSON
}
val jsonProductSyntax: JSON[ClassOfManyTypes] = {
implicit val nestedF: JSON[NestedClass] = jsonProduct(NestedClass.apply _)
jsonProduct(ClassOfManyTypes.apply _)
}

forAll { (x: ClassOfManyTypes) =>
val jsonJP = jsonProductSyntax.write(x)
val jsonD = deriveSyntax.write(x)

jsonJP must be(jsonD)

jsonProductSyntax.read(jsonD).getOrElse(null) must be(x)
deriveSyntax.read(jsonJP).getOrElse(null) must be(x)
}

}

}

object DeriveJSONCompatibilitySpec {

case class NestedClass(id: UUID, str2: String, int2: Int)

case class ClassOfManyTypes(
str: String,
int: Int,
long: Long,
list: List[Int],
mapOfNested: Map[String, NestedClass]
)

implicit val userArbitrary: Arbitrary[NestedClass] = Arbitrary {
for {
id <- Gen.const(UUID.randomUUID())
firstName <- Gen.alphaStr.suchThat(_.nonEmpty)
age <- Gen.chooseNum(1, 120)
} yield NestedClass(id, firstName, age)
}

implicit val cmtArbitrary: Arbitrary[ClassOfManyTypes] = Arbitrary {
for {
str <- Gen.alphaStr.suchThat(_.nonEmpty)
int <- Arbitrary.arbitrary[Int]
long <- Arbitrary.arbitrary[Long]
list <- Gen.listOf(Arbitrary.arbitrary[Int])
map <- Gen.mapOf(Gen.zip(Gen.alphaStr.suchThat(_.nonEmpty), Arbitrary.arbitrary[NestedClass]))
} yield ClassOfManyTypes(str, int, long, list, map)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,29 @@ object JSONEmbeddedSpec {
case class Embedded(value1: String, value2: Int)

object Embedded {
implicit val json: JSON[Embedded] = jsonProduct(apply _)
implicit val json: JSON[Embedded] = deriveJSON
}

case class Test1(name: String, @JSONEmbedded embedded: Embedded)

object Test1 {
implicit val json: JSON[Test1] = jsonProduct(apply _)
implicit val json: JSON[Test1] = deriveJSON
}

case class Test2(name: String, @JSONEmbedded embedded: Option[Embedded] = None)

object Test2 {
implicit val json: JSON[Test2] = jsonProduct(apply _)
implicit val json: JSON[Test2] = deriveJSON
}

case class SubTest4(@JSONEmbedded embedded: Embedded)
object SubTest4 {
implicit val json: JSON[SubTest4] = jsonProduct(apply _)
implicit val json: JSON[SubTest4] = deriveJSON
}

case class Test4(subField: Option[SubTest4] = None)
object Test4 {
implicit val json: JSON[Test4] = jsonProduct(apply _)
implicit val json: JSON[Test4] = deriveJSON
}
}

Expand Down
55 changes: 28 additions & 27 deletions json/json-derivation/src/test/scala/io/sphere/json/JSONSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,14 @@ class JSONSpec extends AnyFunSpec with Matchers {

it("must provide derived instances for sum types with a mix of case class / object") {
implicit val mixedJSON = deriveJSON[Mixed]

List(SingletonMixed, RecordMixed(1)).foreach { m: Mixed =>
fromJSON[Mixed](toJSON(m)) must equal(Valid(m))
}
}

it("must provide derived instances for scala.Enumeration") {
implicit val scalaEnumJSON = deriveJSON[ScalaEnum.Value]
implicit val scalaEnumJSON: JSON[JSONSpec.ScalaEnum.Value] = jsonEnum(ScalaEnum)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot use deriveJSON here?

Copy link
Contributor Author

@benko-ct benko-ct May 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enumerations have a fixed implementation, we don't need to "derive" anything, so I didn't add them to the scala-3 deriveJSON. The code is simpler this way.
If you think it's better to have everything under deriveJSON, I can add it to the scala-3 implementation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we have deriveJSON for enums in scala 2, it would be nice to have the same in scala 3, unless this is too much work. I let you judge here.

ScalaEnum.values.foreach { v =>
val json = s"""[${toJSON(v)}]"""
withClue(json) {
Expand All @@ -188,7 +189,7 @@ class JSONSpec extends AnyFunSpec with Matchers {
}

it("must handle subclasses correctly in `jsonTypeSwitch`") {
implicit val jsonImpl = TestSubjectBase.json
implicit val jsonImpl: JSON[TestSubjectBase] = TestSubjectBase.json

val testSubjects = List[TestSubjectBase](
TestSubjectConcrete1("testSubject1"),
Expand All @@ -211,24 +212,24 @@ class JSONSpec extends AnyFunSpec with Matchers {
describe("ToJSON and FromJSON") {
it("must provide derived JSON instances for sum types") {
// ToJSON
implicit val birdToJSON = toJsonProduct(Bird.apply _)
implicit val dogToJSON = toJsonProduct(Dog.apply _)
implicit val catToJSON = toJsonProduct(Cat.apply _)
implicit val birdToJSON: ToJSON[Bird] = deriveToJSON
implicit val dogToJSON: ToJSON[Dog] = deriveToJSON
implicit val catToJSON: ToJSON[Cat] = deriveToJSON
implicit val animalToJSON = toJsonTypeSwitch[Animal, Bird, Dog, Cat](Nil)
// FromJSON
implicit val birdFromJSON = fromJsonProduct(Bird.apply _)
implicit val dogFromJSON = fromJsonProduct(Dog.apply _)
implicit val catFromJSON = fromJsonProduct(Cat.apply _)
implicit val birdFromJSON: FromJSON[Bird] = deriveFromJSON
implicit val dogFromJSON: FromJSON[Dog] = deriveFromJSON
implicit val catFromJSON: FromJSON[Cat] = deriveFromJSON
implicit val animalFromJSON = fromJsonTypeSwitch[Animal, Bird, Dog, Cat](Nil)

List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { a: Animal =>
List(Bird("Peewee"), Dog("Hasso"), Cat("Felidae")).foreach { (a: Animal) =>
fromJSON[Animal](toJSON(a)) must equal(Valid(a))
}
}

it("must provide derived instances for product types with concrete type parameters") {
implicit val aToJSON = toJsonProduct(GenericA.apply[String] _)
implicit val aFromJSON = fromJsonProduct(GenericA.apply[String] _)
implicit val aToJSON: ToJSON[GenericA[String]] = deriveToJSON
implicit val aFromJSON: FromJSON[GenericA[String]] = deriveFromJSON
val a = GenericA("hello")
fromJSON[GenericA[String]](toJSON(a)) must equal(Valid(a))
}
Expand Down Expand Up @@ -265,12 +266,12 @@ class JSONSpec extends AnyFunSpec with Matchers {

it("must provide derived instances for sum types with a mix of case class / object") {
// ToJSON
implicit val toSingleJSON = toJsonProduct0(SingletonMixed)
implicit val toRecordJSON = toJsonProduct(RecordMixed.apply _)
implicit val toSingleJSON: ToJSON[JSONSpec.SingletonMixed.type] = deriveToJSON
implicit val toRecordJSON: ToJSON[RecordMixed] = deriveToJSON
implicit val toMixedJSON = toJsonTypeSwitch[Mixed, SingletonMixed.type, RecordMixed](Nil)
// FromJSON
implicit val fromSingleJSON = fromJsonProduct0(SingletonMixed)
implicit val fromRecordJSON = fromJsonProduct(RecordMixed.apply _)
implicit val fromSingleJSON: FromJSON[JSONSpec.SingletonMixed.type] = deriveFromJSON
implicit val fromRecordJSON: FromJSON[RecordMixed] = deriveFromJSON
implicit val fromMixedJSON = fromJsonTypeSwitch[Mixed, SingletonMixed.type, RecordMixed](Nil)
List(SingletonMixed, RecordMixed(1)).foreach { m: Mixed =>
fromJSON[Mixed](toJSON(m)) must equal(Valid(m))
Expand All @@ -290,10 +291,10 @@ class JSONSpec extends AnyFunSpec with Matchers {

it("must handle subclasses correctly in `jsonTypeSwitch`") {
// ToJSON
implicit val to1 = toJsonProduct(TestSubjectConcrete1.apply _)
implicit val to2 = toJsonProduct(TestSubjectConcrete2.apply _)
implicit val to3 = toJsonProduct(TestSubjectConcrete3.apply _)
implicit val to4 = toJsonProduct(TestSubjectConcrete4.apply _)
implicit val to1: ToJSON[TestSubjectConcrete1] = deriveToJSON
implicit val to2: ToJSON[TestSubjectConcrete2] = deriveToJSON
implicit val to3: ToJSON[TestSubjectConcrete3] = deriveToJSON
implicit val to4: ToJSON[TestSubjectConcrete4] = deriveToJSON
implicit val toA =
toJsonTypeSwitch[TestSubjectCategoryA, TestSubjectConcrete1, TestSubjectConcrete2](Nil)
implicit val toB =
Expand All @@ -302,10 +303,10 @@ class JSONSpec extends AnyFunSpec with Matchers {
toJsonTypeSwitch[TestSubjectBase, TestSubjectCategoryA, TestSubjectCategoryB](Nil)

// FromJSON
implicit val from1 = fromJsonProduct(TestSubjectConcrete1.apply _)
implicit val from2 = fromJsonProduct(TestSubjectConcrete2.apply _)
implicit val from3 = fromJsonProduct(TestSubjectConcrete3.apply _)
implicit val from4 = fromJsonProduct(TestSubjectConcrete4.apply _)
implicit val from1: FromJSON[TestSubjectConcrete1] = deriveFromJSON
implicit val from2: FromJSON[TestSubjectConcrete2] = deriveFromJSON
implicit val from3: FromJSON[TestSubjectConcrete3] = deriveFromJSON
implicit val from4: FromJSON[TestSubjectConcrete4] = deriveFromJSON
implicit val fromA =
fromJsonTypeSwitch[TestSubjectCategoryA, TestSubjectConcrete1, TestSubjectConcrete2](Nil)
implicit val fromB =
Expand All @@ -332,11 +333,11 @@ class JSONSpec extends AnyFunSpec with Matchers {
it("must provide derived JSON instances for product types (case classes)") {
import JSONSpec.{Milestone, Project}
// ToJSON
implicit val milestoneToJSON = toJsonProduct(Milestone.apply _)
implicit val projectToJSON = toJsonProduct(Project.apply _)
implicit val milestoneToJSON: ToJSON[Milestone] = deriveToJSON
implicit val projectToJSON: ToJSON[Project] = deriveToJSON
// FromJSON
implicit val milestoneFromJSON = fromJsonProduct(Milestone.apply _)
implicit val projectFromJSON = fromJsonProduct(Project.apply _)
implicit val milestoneFromJSON: FromJSON[Milestone] = deriveFromJSON
implicit val projectFromJSON: FromJSON[Project] = deriveFromJSON

val proj =
Project(42, "Linux", 7, Milestone("1.0") :: Milestone("2.0") :: Milestone("3.0") :: Nil)
Expand Down
Loading