能否像jq CLI工具一样用字符串JsonPath使用circe-optics并创建对应Lens?
Great question! Let's break down your two requirements and walk through practical, actionable solutions for each.
1. Mimicking jq-style String JsonPath with circe-optics
Circe doesn't include native support for string-based JsonPath queries directly in circe-optics, but you have two reliable ways to replicate this behavior:
Option A: Leverage circe-jackson for JsonPath parsing
Circe integrates with Jackson's mature JsonPath implementation via the circe-jackson module. You can use it to resolve a JsonPath string to a value, then pair it with optics if you need read/write capabilities. Here's a working example:
import io.circe._, io.circe.jackson.JsonPath import monocle.Optional // Sample JSON structure val sampleJson = Json.obj( "field1" -> Json.obj( "field2" -> Json.arr( Json.obj("field3" -> Json.fromString("ignore")), Json.obj("field3" -> Json.fromString("target value")) ) ) ) // Resolve value via JsonPath string val pathResult: Option[Json] = JsonPath.query("$.field1.field2[1].field3")(sampleJson) // Combine with optics for updates pathResult.foreach { _ => val updatedJson = JsonPath.update("$.field1.field2[1].field3", Json.fromString("new value"))(sampleJson) println(updatedJson.spaces2) }
Option B: Build a custom string-to-optic parser
For tight integration with circe-optics, you can write a lightweight parser that converts a JsonPath-style string into the corresponding Optional (or Lens) step-by-step. This lets you use the full power of optics directly with string paths:
import io.circe.optics.JsonPath._ import monocle.Optional def parseJsonPath(path: String): Optional[Json, Json] = { // Split path into segments, handling both fields and array indices val segments = path.split("\\.").flatMap { segment => if (segment.contains("[")) { val field = segment.takeWhile(_ != '[') val index = segment.dropWhile(_ != '[').drop(1).dropRight(1).toInt List(Left(field), Right(index)) } else { List(Left(segment)) } } // Build the optic by folding through segments segments.foldLeft[Optional[Json, Json]](root) { case (current, Left(field)) => current.field(field) case (current, Right(index)) => current.index(index) } } // Usage example val myOptic = parseJsonPath("field1.field2[1].field3") val targetValue: Option[String] = myOptic.string.getOption(sampleJson) val updatedJson: Json = myOptic.set(Json.fromString("updated value"))(sampleJson)
You can extend this parser to support more JsonPath features (like wildcards or filters) if your use case requires it.
2. Creating a Lens from a String Path
Absolutely! The parseJsonPath function above returns an Optional[Json, Json], which is the safest choice for Json paths (since any segment could be missing). If you know the path will always exist in your JSON structure, you can convert this Optional to a Lens using monocle.Lens.fromOptional with a default value:
import monocle.Lens def parsePathToLens(path: String, fallbackValue: Json): Lens[Json, Json] = { val baseOptional = parseJsonPath(path) Lens.fromOptional(baseOptional)(_ => fallbackValue) } // Usage (assuming path exists) val myLens = parsePathToLens("field1.field2[1].field3", Json.fromString("fallback")) val currentValue: String = myLens.get(sampleJson).asString.get val updatedJson: Json = myLens.set(Json.fromString("new lens value"))(sampleJson)
内容的提问来源于stack exchange,提问作者Bartek Andrzejczak

