Scala JSON diffs

Viewed 978

I have two JSONs with the exact same keys.

val json1 = 
"""{
  'name': 'Henry',
  'age' : 26,
  'activities' : {
                 'school': 'basketball club', 
                 'after-school': 'chess'
                 }
}"""

val json2 = 
"""{
  'name': 'David',
  'age' : 23,
  'activities' : {
                 'school': 'baseball club', 
                 'after-school': 'programming'
                 }
}"""

I would like the difference between the two JSONs, for example, such as:

name = Henry, David
age = 23, 26
activities.school= basketball club, baseball club
activities.after-school=chess, programming

It doesn't have to follow the above format but, I would like to get the keys and values that are differing.

2 Answers

You could try diffson, a circe based library: https://github.com/gnieh/diffson

Example:

import diffson._
import diffson.lcs._
import diffson.circe._
import diffson.jsonpatch._
import diffson.jsonpatch.lcsdiff._

import io.circe._
import io.circe.parser._

import cats._
import cats.implicits._

implicit val lcs = new Patience[Json]

val json1 = parse("""{
                    |  "a": 1,
                    |  "b": true,
                    |  "c": ["test", "plop"]
                    |}""".stripMargin)

val json2 = parse("""{
                    |  "a": 6,
                    |  "c": ["test2", "plop"],
                    |  "d": false
                    |}""".stripMargin)

val patch =
  for {
    json1 <- json1
    json2 <- json2
  } yield diff(json1, json2)

Would return:

[{
  "op":"replace",
  "path":"/a",
  "value":6
},{
  "op":"remove",
  "path":"/b"
},{
  "op":"replace",
  "path":"/c/0",
  "value":"test2"
},{
  "op":"add",
  "path":"/d",
  "value":false
}]

Basically you can concatenate "add" section values for your exact purpose.

As was already suggested by @dk14 you can use diffson librarry: https://github.com/gnieh/diffson - but JsonPatch structure which it provides might be not very convenient for your use case, so it can be converted into another to get result in desired format. Please, see some code example below:

import diffson.jsonpatch.{Add, JsonPatch, Remove, Replace}
  import diffson.jsonpointer.{Part, Pointer}
  import io.circe.Json

  // Model representing plain json diff at certain path, that can be rendered at more human readable format 
  case class JsonPathDiff(path: Pointer, left: Option[Json], right: Option[Json]) {
    def readableString: String = {
      val pathReadableString: String = {
        def partToString(part: Part): String = part.fold(identity, _.toString)
        path.parts.toList.map(partToString).mkString(".")
      }

      def jsonReadableValue(json: Option[Json]): String = json.map(_.toString()).getOrElse("")
      val leftValue = jsonReadableValue(left)
      val rightValue = jsonReadableValue(right)
      s"$pathReadableString = $leftValue , $rightValue"
    }
  }

  // Model representing overall difference between two JSON's
  case class JsonDiff(diff: List[JsonPathDiff]) {
    def readableString: String = diff.map(_.readableString).mkString("\n")
  }

  object JsonDiff {
    def fromPatch(patch: JsonPatch[Json]): JsonDiff = {
      val paths = patch.ops.collect {
        case Add(path, value) => JsonPathDiff(path, None, Some(value))
        case Remove(path, old) => JsonPathDiff(path, old, None)
        case Replace(path, value, old) => JsonPathDiff(path, old, Some(value))
      }
      JsonDiff(paths)
    }
  }



  def main(args: Array[String]): Unit = {
    import diffson._
    import diffson.circe._
    import diffson.jsonpatch._
    import diffson.jsonpatch.lcsdiff.remembering._
    import diffson.lcs._
    import io.circe._
    import io.circe.parser._

    val json1 =
      s"""{
        "name": "Henry",
        "age" : 26,
        "activities" : {
                       "school": "basketball club",
                       "after-school": "chess"
                       }
      }"""

    val json2 =
      s"""{
        "name": "David",
        "age" : 23,
        "activities" : {
                       "school": "baseball club",
                       "after-school": "programming"
                       }
      }"""

    implicit val lcs = new Patience[Json]

    val patch: Either[ParsingFailure, JsonPatch[Json]] =
      for {
        json1 <- parse(json1)
        json2 <- parse(json2)
      } yield diff(json1, json2)

    val jsonDiff = JsonDiff.fromPatch(patch.right.get) // Using `get` for sake of example, avoid in real production code
    println(jsonDiff.readableString)
  }

which will produce next result:

activities.after-school = "chess" , "programming"
activities.school = "basketball club" , "baseball club"
age = 26 , 23
name = "Henry" , "David"

Hope this helps!

Related