如何在Scala中使用Monocle(Lens)解析深度嵌套JSON提取目标键值映射
我之前看过使用Monocle解析深度嵌套JSON的示例,但现有示例都比较简单,我遇到了结构更复杂的JSON需要处理,结构如下:
{ "products": { "HY3BZPP2B6K8MSJF" : { "sku" : "HY3BZPP2B6K8MSJF", "productFamily" : "Storage", "attributes" : { "servicecode" : "AmazonEC2", "location" : "US East (N. Virginia)", "locationType" : "AWS Region", "storageMedia" : "SSD-backed", "volumeType" : "General Purpose", "maxVolumeSize" : "16 TiB", "maxIopsvolume" : "16000", "maxIopsBurstPerformance" : "3000 for volumes <= 1 TiB", "maxThroughputvolume" : "250 MiB/s", "usagetype" : "EBS:VolumeUsage.gp2", "operation" : "", "servicename" : "Amazon Elastic Compute Cloud", "volumeApiName" : "gp2" } }, ... }, "terms" : { "OnDemand" : { "HY3BZPP2B6K8MSJF" : { "HY3BZPP2B6K8MSJF.JRTCKXETXF" : { "offerTermCode" : "JRTCKXETXF", "sku" : "HY3BZPP2B6K8MSJF", "effectiveDate" : "2021-10-01T00:00:00Z", "priceDimensions" : { "HY3BZPP2B6K8MSJF.JRTCKXETXF.6YS6EN2CT7" : { "rateCode" : "HY3BZPP2B6K8MSJF.JRTCKXETXF.6YS6EN2CT7", "description" : "$0.10 per GB-month of General Purpose SSD (gp2) provisioned storage - US East (Northern Virginia)", "beginRange" : "0", "endRange" : "Inf", "unit" : "GB-Mo", "pricePerUnit" : { "USD" : "0.1000000000" }, "appliesTo" : [ ] } }, "termAttributes" : { } } }, ... }我目前可以通过如下代码完成基础解析:
val productData = productConnector.getProducts(100).map(p => Serializer.fromJson[ProductInfo](p))但我不知道如何编写对应的Lens来提取生成键为volumeApiName、值为pricePerUnit的Map,比如(gp2 -> 0.1000000000)的格式。注意可以通过volumeApiName对应的sku作为关联字段,匹配terms下OnDemand节点的对应内容后逐层深入获取pricePerUnit的值。
实现代码
首先保证反序列化用的ProductInfo样例类层级和JSON结构对齐,参考样例类定义和提取逻辑如下:
import monocle.syntax.all._ import monocle.function.all._ import monocle.gen.all._ // 层级样例类,可根据你实际的序列化结构调整字段 case class PricePerUnit(USD: String) case class PriceDimension(pricePerUnit: PricePerUnit) case class OfferTerm(priceDimensions: Map[String, PriceDimension]) case class OnDemandSkuEntry(terms: Map[String, OfferTerm]) case class Terms(OnDemand: Map[String, OnDemandSkuEntry]) case class ProductAttributes(volumeApiName: String) case class Product(sku: String, attributes: ProductAttributes) case class ProductInfo(products: Map[String, Product], terms: Terms) // 提取目标Map的逻辑 val volumePriceMap: Map[String, BigDecimal] = productData.flatMap { info => info.products.values.flatMap { product => // 用sku作为关联键,逐层查询对应价格 val priceOpt = info.focus(_.terms.OnDemand) .index(product.sku) // 匹配OnDemand下对应sku的条目 .focus(_.terms.values.headOption) // 每个sku下仅对应1个offer term,取第一个 .some .focus(_.priceDimensions.values.headOption) // 每个offer term下仅对应1个计费维度,取第一个 .some .focus(_.pricePerUnit.USD) .getOption .map(BigDecimal(_)) priceOpt.map(price => product.attributes.volumeApiName -> price) } }.toMap
要点说明
- 动态Map键的访问使用Monocle提供的
index方法,自动处理键不存在的情况 - 可选值穿透使用
some语法,任意一层节点不存在时,getOption会直接返回None,无需手动做空判断 - 最终返回的
volumePriceMap就是你需要的volumeApiName对应USD价格的映射,价格已经转成BigDecimal类型方便后续计算
内容的提问来源于stack exchange,提问作者happygopher
相关产品推荐
相关产品推荐

