Skip to content

Instantly share code, notes, and snippets.

@ShahOdin
Last active June 16, 2021 20:25
Show Gist options
  • Save ShahOdin/cb16b6989ffc382d9962075fa7dba243 to your computer and use it in GitHub Desktop.
Save ShahOdin/cb16b6989ffc382d9962075fa7dba243 to your computer and use it in GitHub Desktop.
flatten and unflatten json using circe
import cats.data.NonEmptyList
import io.circe.{ACursor, Json, JsonObject}
import cats.instances.option._
object Foo {
def flattenJson(input: Json): Json = {
var result: Json = Json.obj()
def recurse(json: Json, prop: String): Unit = {
def addTopLevelField(): Unit = {
result = result.deepMerge(Json.obj(prop -> json))
}
def addVector(array: Vector[Json]): Unit =
NonEmptyList
.fromList(array.toList.zipWithIndex)
.fold {
result = result.deepMerge(Json.obj(prop -> Json.arr()))
}(
_.toList.foreach {
case (json, i) =>
recurse(json, s"${prop}[${i}]")
}
)
def addObject(jsonObject: JsonObject): Unit = {
var isEmpty = true
jsonObject.toMap.foreach {
case (p, child) =>
isEmpty = false
recurse(child, if (prop != "") s"${prop}.${p}" else p)
}
if (isEmpty)
result.deepMerge(Json.obj(prop -> Json.obj()))
}
json.arrayOrObject(
or = addTopLevelField(),
jsonArray = addVector,
jsonObject = addObject
)
}
recurse(input, "")
result
}
private def extractNestedArray(
json: Json
): Option[(NonEmptyList[String], Vector[Json])] = {
def extractNestedArrayInternal(
acum: List[String],
json: Json
): Option[(NonEmptyList[String], Vector[Json])] =
json.arrayOrObject(
or = None,
jsonArray = jsons => NonEmptyList.fromList(acum).map(_ -> jsons),
jsonObject = _.toList.headOption.flatMap {
case (k, v) =>
extractNestedArrayInternal(k :: acum, v)
}
)
extractNestedArrayInternal(Nil, json).map {
case (keys, jsons) =>
keys.reverse -> jsons
}
}
private def concatArrayValueAtPath(
json: Json,
path: NonEmptyList[String],
newValues: Vector[Json]
): Option[Json] =
path
.foldLeft[ACursor](
json.hcursor
) {
case (cursor, fieldName) =>
cursor.downField(fieldName)
}
.withFocusM(
_.asArray
.map(_.appendedAll(newValues))
.map(Json.fromValues)
)
.flatMap(_.top)
implicit class jsonOps(json: Json) {
def deepMergeAndConcatArrays(that: Json): Json =
extractNestedArray(that)
.flatMap {
case (arrayPath, those) =>
concatArrayValueAtPath(
json = json,
path = arrayPath,
newValues = those
)
}
.getOrElse(
json.deepMerge(that)
)
}
private val Dotted = "([^\\.]*)\\.(.*)".r
private val Bracketted = "([^\\.]*)\\[(\\d*)\\]".r
def unFlattenJson(json: Json): Json =
json.arrayOrObject(
json,
js => Json.fromValues(js.map(unFlattenJson)),
_.toList
.map {
case (Dotted(k, rest), v) =>
Json.obj(k -> unFlattenJson(Json.obj(rest -> v)))
case (Bracketted(k, _), v) =>
Json.obj(k -> unFlattenJson(Json.arr(v)))
case (k, v) => Json.obj(k -> unFlattenJson(v))
}
.reduceOption[Json](_ deepMergeAndConcatArrays _)
.getOrElse(Json.obj())
)
}
@ShahOdin
Copy link
Author

  val json = Json.obj(
    "foo" -> Json.obj(
      "bar" -> "text".asJson,
      "baz" -> 123.asJson,
      "bam" -> List(10, 11, 12).asJson
    ),
    "qux" -> 456.asJson,
    "myArray" -> List(7, 8, 9).asJson
  )

  val flattenedJson = Json.obj(
    "foo.bar" -> "text".asJson,
    "foo.baz" -> 123.asJson,
    "foo.bam[0]" -> 10.asJson,
    "foo.bam[1]" -> 11.asJson,
    "foo.bam[2]" -> 12.asJson,
    "qux" -> 456.asJson,
    "myArray[0]" -> 7.asJson,
    "myArray[1]" -> 8.asJson,
    "myArray[2]" -> 9.asJson
  )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment