Scala 2.13:为多类型值创建通用Case Class
解决Scala Case Class兼容多类型Value字段的问题
问题描述
给定如下JSON Schema片段:
"elements": { "{element1Name}": { "value": "<p>The HTML content of the element.</p>", ":type": "text/html", "variations": { "{variation1Name}": { "value": "<p>The HTML content of the element.</p>", ":type": "text/html" }, "{variation2Name}": { "value": "<p>The HTML content of the element.</p>", ":type": "text/html" }, "{elementNName}": { "value": "<p>The HTML content of the element.</p>", ":type": "text/html" } } }, "{element2Name}": { "value": "<p>The HTML content of the element.</p>", ":type": "text/html", "variations": { "{variation1Name}": { "value": "<p>The HTML content of the element.</p>", ":type": "text/html" }, "{variation2Name}": { "value": "<p>The HTML content of the element.</p>", ":type": "text/html" }, "{elementNName}": { "value": "<p>The HTML content of the element.</p>", ":type": "text/html" } } }, "{elementNName}": { "value": "<p>The HTML content of the element.</p>", ":type": "text/html", "variations": { "{variation1Name}": { "value": "<p>The HTML content of the element.</p>", ":type": "text/html" }, "{variation2Name}": { "value": "<p>The HTML content of the element.</p>", ":type": "text/html" }, "{elementNName}": { "value": "<p>The HTML content of the element.</p>", ":type": "text/html" } } } }
当前已实现的Scala Case Class结构如下:
case class AssetAPIEntity( value: String, @key(":type") `type`: Option[String] = None, ) case class AssetAPIElements( value: String, @key(":type") `type`: Option[String] = None, variations: Map[String, AssetAPIEntity] ) case class AssetAPIData( title: Option[String] = None, description: Option[String] = None, @key("cq:Model") cqModel: String, elements: Map[String, AssetAPIElements] )
需要修改value字段以支持以下6种类型:
- 单个数字
- 单个布尔值
- 单个字符串
- 数字序列
- 布尔值序列
- 字符串序列
两种可行方案
方案1:代数数据类型(ADT)
这是Scala处理多类型场景的类型安全方案,适合需要严格类型检查的场景。
定义Value类型的ADT
// 密封特质,限制实现范围 sealed trait Value // 单值实现 case class SingleString(value: String) extends Value case class SingleNumber(value: Double) extends Value // 用Double兼容整数和浮点数 case class SingleBoolean(value: Boolean) extends Value // 序列值实现 case class MultiString(value: Seq[String]) extends Value case class MultiNumber(value: Seq[Double]) extends Value case class MultiBoolean(value: Seq[Boolean]) extends Value
修改原有Case Class
将value字段的类型替换为Value:
import io.circe._ import io.circe.generic.semiauto._ case class AssetAPIEntity( value: Value, @key(":type") `type`: Option[String] = None, ) case class AssetAPIElements( value: Value, @key(":type") `type`: Option[String] = None, variations: Map[String, AssetAPIEntity] ) case class AssetAPIData( title: Option[String] = None, description: Option[String] = None, @key("cq:Model") cqModel: String, elements: Map[String, AssetAPIElements] )
实现JSON编解码器(以Circe为例)
需要为Value类型手动编写编解码器,确保JSON和Scala类型正确转换:
// 编码器 implicit val valueEncoder: Encoder[Value] = Encoder.instance { case SingleString(v) => Json.fromString(v) case SingleNumber(v) => Json.fromDoubleOrNull(v) case SingleBoolean(v) => Json.fromBoolean(v) case MultiString(v) => Json.fromValues(v.map(Json.fromString)) case MultiNumber(v) => Json.fromValues(v.map(Json.fromDoubleOrNull)) case MultiBoolean(v) => Json.fromValues(v.map(Json.fromBoolean)) } // 解码器 implicit val valueDecoder: Decoder[Value] = Decoder.instance { c => c.as[String].map(SingleString) orElse c.as[Double].map(SingleNumber) orElse c.as[Boolean].map(SingleBoolean) orElse c.as[Seq[String]].map(MultiString) orElse c.as[Seq[Double]].map(MultiNumber) orElse c.as[Seq[Boolean]].map(MultiBoolean) } // 自动生成其他类的编解码器 implicit val assetAPIEntityCodec: Codec[AssetAPIEntity] = deriveCodec implicit val assetAPIElementsCodec: Codec[AssetAPIElements] = deriveCodec implicit val assetAPIDataCodec: Codec[AssetAPIData] = deriveCodec
方案2:Scala 3联合类型
如果使用Scala 3,可以直接用联合类型简化代码,无需额外定义ADT。
定义联合类型别名
import io.circe._ import io.circe.generic.semiauto._ // 定义支持的所有类型的联合别名 type ValueType = String | Double | Boolean | Seq[String] | Seq[Double] | Seq[Boolean]
修改原有Case Class
将value字段类型替换为ValueType:
case class AssetAPIEntity( value: ValueType, @key(":type") `type`: Option[String] = None, ) case class AssetAPIElements( value: ValueType, @key(":type") `type`: Option[String] = None, variations: Map[String, AssetAPIEntity] ) case class AssetAPIData( title: Option[String] = None, description: Option[String] = None, @key("cq:Model") cqModel: String, elements: Map[String, AssetAPIElements] )
实现JSON编解码器(以Circe为例)
// 编码器 implicit val valueTypeEncoder: Encoder[ValueType] = Encoder.instance { case s: String => Json.fromString(s) case n: Double => Json.fromDoubleOrNull(n) case b: Boolean => Json.fromBoolean(b) case ss: Seq[String] => Json.fromValues(ss.map(Json.fromString)) case ns: Seq[Double] => Json.fromValues(ns.map(Json.fromDoubleOrNull)) case bs: Seq[Boolean] => Json.fromValues(bs.map(Json.fromBoolean)) } // 解码器 implicit val valueTypeDecoder: Decoder[ValueType] = Decoder.instance { c => c.as[String].widen[ValueType] orElse c.as[Double].widen[ValueType] orElse c.as[Boolean].widen[ValueType] orElse c.as[Seq[String]].widen[ValueType] orElse c.as[Seq[Double]].widen[ValueType] orElse c.as[Seq[Boolean]].widen[ValueType] } // 自动生成其他类的编解码器 implicit val assetAPIEntityCodec: Codec[AssetAPIEntity] = deriveCodec implicit val assetAPIElementsCodec: Codec[AssetAPIElements] = deriveCodec implicit val assetAPIDataCodec: Codec[AssetAPIData] = deriveCodec
方案选择建议
- ADT方案:类型安全,模式匹配时编译器会强制检查所有分支,避免遗漏;便于后续扩展新类型,适合大型项目或对类型严格要求的场景。
- 联合类型方案:代码更简洁,无需额外定义case class,但类型安全性稍弱(模式匹配无强制分支检查),仅适用于Scala 3环境,适合小型项目或快速开发场景。
内容的提问来源于stack exchange,提问作者Shivam Sahil
相关产品推荐
相关产品推荐

