You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过Json注解将自定义泛型类Tree[T]序列化为Json?

Akka中泛型Tree[T]的JSON序列化解决方案

问题场景与需求

现有代码结构

  1. 泛型树容器定义:
trait NodeId {
  def id: Int
}

trait Tree[T <: NodeId] {
  def find(id: Int): Option[T]
  def size: Int
  def root: T
  def append(node: T, to: T): Tree[T]
  def remove(id: Int): Tree[T]
}

object Tree {
  val RootId : Int = 0
  val EmptyId: Int = -1
  
  def apply[T <: NodeId](root: T): Tree[T] = {
    if (root == null || root.id != RootId)
      throw new IllegalArgumentException("")
    else
      TreeImpl(root :: Nil)
  }
  
  private case class Node(id: Int, child: Int = EmptyId, sibling: Int = EmptyId)
  
  private case class TreeImpl[T <: NodeId](list            : List[T],
                                           private val tree: List[Node] = Node(RootId) :: Nil)
    extends Tree[T] {
    
    override def find(id: Int): Option[T] = list.find(_.id == id)
    
    def size: Int = list.size
    
    def root: T = this.find(RootId).get
    
    override def append(node: T, to: T): Tree[T] = { /* 实现代码 */ }

    override def remove(id: Int): Tree[T] = { /* 实现代码 */ }      
  }
}
  1. 派生类与聚合根定义:
case class Stage(id         : Int,
                 title      : String
                ) extends NodeId

object Stage {
  val Root: Stage = Stage(id = Tree.RootId, title = "ROOT")
}

trait Aggregate extends CborSerializable {
  type State <: Aggregate
  def id: String
}

trait Plan extends Aggregate {
  type State = Plan
  
  def projectId: String  
  def stages: Tree[Stage]
}

final case class PlacedPlan(id: String,
                            projectId: String,
                            stages: Tree[Stage] = Tree[Stage](Stage.Root)) extends Plan { /* 实现代码 */ }

遇到的问题

序列化PlacedPlan时抛出错误:State [PlacedPlan(...),TreeImpl(List(...),List(...)] isn't serializable.,尝试添加@JsonTypeInfo和@JsonSubTypes注解无效。

目标需求

需要将Tree[Stage]序列化为指定JSON结构,对应以下DTO类(匹配JSON字段名):

import com.fasterxml.jackson.annotation.JsonProperty

case class ProjectDTO (
  projectId: String,
  @JsonProperty("Stages") stages: StagesDTO
)

case class StagesDTO (
  @JsonProperty("stage") stage: List[Stage],
  @JsonProperty("nodes") nodes: List[NodeDTO]
)

case class NodeDTO (
  id: Int,
  child: Int,
  sibling: Int
)

目标JSON结构:

{
    "projectId": "proj-123",
    "Stages": {
        "stage": [
            {"id": 0, "title": "ROOT"},
            {"id": 1, "title": "Stage 1"}
        ],
        "nodes": [
            {"id": 0, "child": 1, "sibling": -1},
            {"id": 1, "child": -1, "sibling": -1}
        ]
    }
}

解决方案

1. 实现业务模型与DTO的转换逻辑

由于TreeImpl是私有类,通过扩展方法安全暴露内部数据,避免反射风险:

import scala.reflect.ClassTag

object TreeExtensions {
  implicit class TreeOps[T <: NodeId](tree: Tree[T]) {
    def toStagesDTO(implicit tag: ClassTag[T]): StagesDTO = {
      tree match {
        case impl: Tree.TreeImpl[T] =>
          val nodes = impl.tree.map(node => NodeDTO(node.id, node.child, node.sibling))
          StagesDTO(impl.list, nodes)
        case _ => throw new IllegalArgumentException("Unsupported Tree implementation")
      }
    }
  }

  // 反序列化:从DTO重建Tree[T]
  def fromStagesDTO[T <: NodeId](dto: StagesDTO)(implicit tag: ClassTag[T]): Tree[T] = {
    val root = dto.stage.find(_.id == Tree.RootId)
      .getOrElse(throw new IllegalArgumentException("Missing root node in DTO"))
    val treeNodes = dto.nodes.map(n => Tree.Node(n.id, n.child, n.sibling))
    Tree.TreeImpl(dto.stage, treeNodes)
  }
}

2. 编写自定义Jackson序列化器

实现Tree[Stage]与StagesDTO之间的序列化/反序列化逻辑:

import com.fasterxml.jackson.core.{JsonGenerator, JsonParser}
import com.fasterxml.jackson.databind.{DeserializationContext, SerializerProvider, JsonSerializer, JsonDeserializer}
import com.fasterxml.jackson.databind.module.SimpleModule

class TreeStageSerializer extends JsonSerializer[Tree[Stage]] {
  override def serialize(value: Tree[Stage], gen: JsonGenerator, serializers: SerializerProvider): Unit = {
    import TreeExtensions._
    val dto = value.toStagesDTO
    serializers.defaultSerializeValue(dto, gen)
  }
}

class TreeStageDeserializer extends JsonDeserializer[Tree[Stage]] {
  override def deserialize(p: JsonParser, ctxt: DeserializationContext): Tree[Stage] = {
    import TreeExtensions._
    val dto = ctxt.readValue(p, classOf[StagesDTO])
    fromStagesDTO(dto)
  }
}

3. 配置Akka序列化绑定

在application.conf中注册自定义序列化器:

akka {
  serialization {
    serializers {
      tree-stage-json = "your.package.TreeStageSerializer"
    }
    bindings {
      "your.package.Tree[your.package.Stage]" = tree-stage-json
      "your.package.StagesDTO" = jackson-json
      "your.package.ProjectDTO" = jackson-json
    }
  }
}

若使用Akka HTTP的Jackson marshaller,可通过代码注册模块:

import akka.http.scaladsl.marshallers.jackson.Jackson
import com.fasterxml.jackson.databind.ObjectMapper

val mapper = new ObjectMapper()
val module = new SimpleModule()
module.addSerializer(classOf[Tree[Stage]], new TreeStageSerializer())
module.addDeserializer(classOf[Tree[Stage]], new TreeStageDeserializer())
mapper.registerModule(module)

// 全局生效的Jackson marshaller
implicit val jacksonMarshaller = Jackson.marshaller(mapper)

4. 聚合根与DTO的转换(可选)

若需要直接序列化PlacedPlan到目标JSON,添加转换方法:

implicit class PlacedPlanOps(plan: PlacedPlan) {
  def toProjectDTO: ProjectDTO = {
    import TreeExtensions._
    ProjectDTO(plan.projectId, plan.stages.toStagesDTO)
  }
}

// 反序列化示例
def toPlacedPlan(dto: ProjectDTO): PlacedPlan = {
  import TreeExtensions._
  PlacedPlan(
    id = s"plan-${dto.projectId}",
    projectId = dto.projectId,
    stages = fromStagesDTO(dto.stages)
  )
}

关键说明

  • 通过扩展方法访问TreeImpl内部数据,避免直接反射带来的维护风险
  • 自定义序列化器直接对接业务模型与DTO,严格匹配目标JSON结构
  • 配置Akka序列化绑定后,框架会自动处理Tree[Stage]类型的序列化逻辑

内容的提问来源于stack exchange,提问作者Caspar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 09:08:18