Last active
June 16, 2021 20:25
-
-
Save ShahOdin/cb16b6989ffc382d9962075fa7dba243 to your computer and use it in GitHub Desktop.
flatten and unflatten json using circe
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
There is probably a much more concise impl of these two but this seems to work.
flatten impl was ported from https://stackoverflow.com/questions/19098797/fastest-way-to-flatten-un-flatten-nested-json-objects/19101235#19101235
unflatten was inspired by Travis's impl which doesn't support arrays: https://stackoverflow.com/questions/44057439/use-circe-to-preprocess-dot-notation-style-fields/44059753#44059753
I have tested it with: