Repository: scala-steward-org/scala-steward (scala)

Issue:
JSON parsing error when decoding pull_requests.json
Hi, 

I've decided to give a try to the new features available on the 0.16.0, especially the grouping one. The problem is that I see some errors, when deserializing the `pull_requests.json` which is an internal file to the Steward workspace. 

Below is the JSON that Steward tried to parse: 
```json
{
  "https://github.com/ContentSquare/de_steward-test-repository/pull/69" : {
    "baseSha1" : "2b7603f40dcfb122e84e65f19905be8c9c660d36",
    "update" : {
      "Grouped" : {
        "name" : "all",
        "title" : null,
        "updates" : [
          {
            "crossDependency" : [
              {
                "groupId" : "org.scala-lang",
                "artifactId" : {
                  "name" : "scala-library",
                  "maybeCrossName" : null
                },
                "version" : "2.12.17",
                "sbtVersion" : null,
                "scalaVersion" : null,
                "configurations" : "provided"
              }
            ],
            "newerVersions" : [
              "2.13.10"
            ],
            "newerGroupId" : null,
            "newerArtifactId" : null
          }
        ]
      }
    },
    "state" : "closed",
    "entryCreatedAt" : 1669725032488,
    "number" : 69,
    "updateBranch" : "update/all"
  }
}
```

And the error says: 
```
ERROR Failed to parse or decode JSON from /opt/docker/workspace/store/pull_requests/v2/github/ContentSquare/de_steward-test-repository/pull_requests.json
io.circe.DecodingFailure$DecodingFailureImpl: Missing required field: DownField(Single),DownArray,DownField(updates),DownField(Grouped),DownField(update),DownField(https://github.com/ContentSquare/de_steward-test-repository/pull/69)
```

Since the internal looks local to Steward, I'm not sure how can I do more on my side. I've already cleanup my workspace and the errors still occurs. Can you please help?

Pull request:
> ❗ This must be released as a patch version ASAP.

## 🐛 The bug

`Update` decoders are expecting JSON objects to be wrapped in a field named as the class:

```json
"ForArtifactId": {
   ...
}
```

While individual encoders are not creating this field.

This only happens when decoding `Grouped` updates, since it is the only place an individual encoder was being used.

## 👨🏼‍💻 The solution

This PR ensures encoders always introduce this wrapper-field so updates are correctly parsed on every situation.

## References

Fixes #2802

Patch:
diff --git a/modules/core/src/main/scala/org/scalasteward/core/data/Update.scala b/modules/core/src/main/scala/org/scalasteward/core/data/Update.scala
index c589a4ca77..295b8cb468 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/data/Update.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/data/Update.scala
@@ -18,13 +18,15 @@ package org.scalasteward.core.data
 
 import cats.Order
 import cats.implicits._
+import io.circe.Decoder
+import io.circe.Encoder
+import io.circe.Json
 import io.circe.generic.semiauto._
+import io.circe.syntax._
 import org.scalasteward.core.repoconfig.PullRequestGroup
 import org.scalasteward.core.util
 import org.scalasteward.core.util.Nel
 import org.scalasteward.core.util.string.MinLengthString
-import io.circe.Decoder
-import io.circe.Encoder
 
 sealed trait Update {
 
@@ -193,7 +195,11 @@ object Update {
   // Partially manually implemented so we don't fail reading old caches (those
   // still using `Single`/`Group`)
 
-  implicit val ForArtifactIdEncoder: Encoder[ForArtifactId] = deriveEncoder
+  implicit val ForArtifactIdEncoder: Encoder[ForArtifactId] = {
+    val derived = deriveEncoder[ForArtifactId]
+
+    derived.mapJson(json => Json.obj("ForArtifactId" -> json))
+  }
 
   implicit val ForArtifactIdDecoder: Decoder[ForArtifactId] = {
     val derived = deriveDecoder[ForArtifactId]
@@ -202,7 +208,11 @@ object Update {
       .or(derived.prepare(_.downField("Single")))
   }
 
-  implicit val ForGroupIdEncoder: Encoder[ForGroupId] = deriveEncoder
+  implicit val ForGroupIdEncoder: Encoder[ForGroupId] = {
+    val derived = deriveEncoder[ForGroupId]
+
+    derived.mapJson(json => Json.obj("ForGroupId" -> json))
+  }
 
   implicit val ForGroupIdDecoder: Decoder[ForGroupId] = {
     val derived = deriveDecoder[ForGroupId]
@@ -211,7 +221,11 @@ object Update {
       .or(derived.prepare(_.downField("Group")))
   }
 
-  implicit val GroupedEncoder: Encoder[Grouped] = deriveEncoder
+  implicit val GroupedEncoder: Encoder[Grouped] = {
+    val derived = deriveEncoder[Grouped]
+
+    derived.mapJson(json => Json.obj("Grouped" -> json))
+  }
 
   implicit val GroupedDecoder: Decoder[Grouped] =
     deriveDecoder[Grouped].prepare(_.downField("Grouped"))
@@ -219,7 +233,11 @@ object Update {
   implicit val SingleOrder: Order[Single] =
     Order.by((u: Single) => (u.crossDependencies, u.newerVersions))
 
-  implicit val UpdateEncoder: Encoder[Update] = deriveEncoder
+  implicit val UpdateEncoder: Encoder[Update] = {
+    case update: Grouped       => update.asJson
+    case update: ForArtifactId => update.asJson
+    case update: ForGroupId    => update.asJson
+  }
 
   implicit val UpdateDecoder: Decoder[Update] =
     ForArtifactIdDecoder


Test patch:
diff --git a/modules/core/src/test/scala/org/scalasteward/core/data/UpdateTest.scala b/modules/core/src/test/scala/org/scalasteward/core/data/UpdateTest.scala
index 901a073c0d..52b162a2ed 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/data/UpdateTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/data/UpdateTest.scala
@@ -3,6 +3,8 @@ package org.scalasteward.core.data
 import munit.FunSuite
 import org.scalasteward.core.TestSyntax._
 import org.scalasteward.core.util.Nel
+import io.circe.syntax._
+import io.circe.Json
 
 class UpdateTest extends FunSuite {
   test("Group.mainArtifactId") {
@@ -67,4 +69,135 @@ class UpdateTest extends FunSuite {
       ("org.scala-sbt".g % Nel.of("sbt-launch".a, "scripted-plugin".a) % "1.2.1" %> "1.2.4").group
     assertEquals(update.show, "org.scala-sbt:{sbt-launch, scripted-plugin} : 1.2.1 -> 1.2.4")
   }
+
+  test("ForArtifactId Encoder/Decoder") {
+    val update1: Update =
+      ("org.specs2".g % "specs2-core".a % "3.9.4" % "test" %> "3.9.5").single
+    val update2: Update.ForArtifactId =
+      ("org.specs2".g % "specs2-core".a % "3.9.4" % "test" %> "3.9.5").single
+
+    val expected = Json.obj(
+      "ForArtifactId" := Json.obj(
+        "crossDependency" := Json.arr(
+          Json.obj(
+            "groupId" := "org.specs2",
+            "artifactId" := Json.obj(
+              "name" := "specs2-core",
+              "maybeCrossName" := None
+            ),
+            "version" := "3.9.4",
+            "sbtVersion" := None,
+            "scalaVersion" := None,
+            "configurations" := "test"
+          )
+        ),
+        "newerVersions" := List("3.9.5"),
+        "newerGroupId" := None,
+        "newerArtifactId" := None
+      )
+    )
+
+    assertEquals(update1.asJson, expected)
+    assertEquals(update1.asJson, update2.asJson)
+    assertEquals(update1.asJson.as[Update], update2.asJson.as[Update])
+    assertEquals(update1.asJson.as[Update.ForArtifactId], update2.asJson.as[Update.ForArtifactId])
+    assertEquals(update1.asJson.as[Update], update2.asJson.as[Update.ForArtifactId])
+  }
+
+  test("ForGroupId Encoder/Decoder") {
+    val update1: Update =
+      ("org.scala-sbt".g % Nel.of("sbt-launch".a, "scripted-plugin".a) % "1.2.1" %> "1.2.4").group
+    val update2: Update.ForGroupId =
+      ("org.scala-sbt".g % Nel.of("sbt-launch".a, "scripted-plugin".a) % "1.2.1" %> "1.2.4").group
+
+    val expected = Json.obj(
+      "ForGroupId" := Json.obj(
+        "crossDependencies" := Json.arr(
+          Json.arr(
+            Json.obj(
+              "groupId" := "org.scala-sbt",
+              "artifactId" := Json.obj(
+                "name" := "sbt-launch",
+                "maybeCrossName" := None
+              ),
+              "version" := "1.2.1",
+              "sbtVersion" := None,
+              "scalaVersion" := None,
+              "configurations" := None
+            )
+          ),
+          Json.arr(
+            Json.obj(
+              "groupId" := "org.scala-sbt",
+              "artifactId" := Json.obj(
+                "name" := "scripted-plugin",
+                "maybeCrossName" := None
+              ),
+              "version" := "1.2.1",
+              "sbtVersion" := None,
+              "scalaVersion" := None,
+              "configurations" := None
+            )
+          )
+        ),
+        "newerVersions" := List("1.2.4")
+      )
+    )
+
+    assertEquals(update1.asJson, expected)
+    assertEquals(update1.asJson, update2.asJson)
+    assertEquals(update1.asJson.as[Update], update2.asJson.as[Update])
+    assertEquals(update1.asJson.as[Update.ForGroupId], update2.asJson.as[Update.ForGroupId])
+    assertEquals(update1.asJson.as[Update], update2.asJson.as[Update.ForGroupId])
+  }
+
+  test("Grouped Encoder/Decoder") {
+    val update1: Update =
+      Update.Grouped(
+        name = "all",
+        title = Some("All"),
+        updates = List(("org.specs2".g % "specs2-core".a % "3.9.4" % "test" %> "3.9.5").single)
+      )
+    val update2: Update.Grouped =
+      Update.Grouped(
+        name = "all",
+        title = Some("All"),
+        updates = List(("org.specs2".g % "specs2-core".a % "3.9.4" % "test" %> "3.9.5").single)
+      )
+
+    val expected = Json.obj(
+      "Grouped" := Json.obj(
+        "name" := "all",
+        "title" := "All",
+        "updates" := Json.arr(
+          Json.obj(
+            "ForArtifactId" := Json.obj(
+              "crossDependency" := Json.arr(
+                Json.obj(
+                  "groupId" := "org.specs2",
+                  "artifactId" := Json.obj(
+                    "name" := "specs2-core",
+                    "maybeCrossName" := None
+                  ),
+                  "version" := "3.9.4",
+                  "sbtVersion" := None,
+                  "scalaVersion" := None,
+                  "configurations" := "test"
+                )
+              ),
+              "newerVersions" := List("3.9.5"),
+              "newerGroupId" := None,
+              "newerArtifactId" := None
+            )
+          )
+        )
+      )
+    )
+
+    assertEquals(update1.asJson, expected)
+    assertEquals(update1.asJson, update2.asJson)
+    assertEquals(update1.asJson.as[Update], update2.asJson.as[Update])
+    assertEquals(update1.asJson.as[Update.Grouped], update2.asJson.as[Update.Grouped])
+    assertEquals(update1.asJson.as[Update], update2.asJson.as[Update.Grouped])
+  }
 }