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

Scala 3中如何从Option提取参数化类型的java.lang.reflect.Type?

问题描述

核心需求(TLDR)

给定classOf[Option[String]]这类类型,需提取其内部参数化类型java.lang.String。

背景说明

我有一个使用GSON进行序列化/反序列化的Scala 2.13项目,其中实现的Option编解码器可正常工作,代码如下:

import java.lang.reflect.Type
import scala.reflect.ClassTag

import com.google.gson._
import java.lang.reflect.ParameterizedType

class OptionSerializer extends JsonSerializer[Option[Any]] with JsonDeserializer[Option[Any]] {


  private def innerType(outerType: Type): Type = 
    outerType match {
      case pt: ParameterizedType => pt.getActualTypeArguments()(0)
      case _                     => throw new UnsupportedOperationException("Expected ParameterizedType")
    }
  

  override def serialize(
      src: Option[Any],
      typeOfSrc: Type,
      context: JsonSerializationContext
  ): JsonElement = {
    src match {
      case None    => JsonNull.INSTANCE
      case Some(v) => context.serialize(v, innerType(typeOfSrc))
    }
  }
  override def deserialize(
      json: JsonElement,
      typeOfT: Type,
      context: JsonDeserializationContext
  ): Option[Any] = {
    json match {
      case null                 => None
      case _ if json.isJsonNull => None
      case _                    => Some(context.deserialize(json, innerType(typeOfT)))
    }
  }
}

切换到Scala 3后,上述代码失效,原因是Scala 3中的Option不再是java.lang.reflect.ParameterizedType,需要重写innerType方法适配Scala 3。

项目代码库规模庞大,无法迁移到Circe等其他序列化框架,仅该问题阻碍Scala 3迁移。

可复现代码

使用scala-cli可运行的示例代码,将using scala行改为//> using scala "3.3.1"即可复现问题:

#!/usr/bin/env -S scala-cli shebang
// "2.13.12" or "3.3.1"
//> using scala "2.13.12" 
//> using lib "com.google.code.gson:gson:2.10.1"

import java.lang.reflect.Type
import scala.reflect.ClassTag

import com.google.gson._
import java.lang.reflect.ParameterizedType

class OptionSerializer extends JsonSerializer[Option[Any]] with JsonDeserializer[Option[Any]] {


  private def innerType(outerType: Type): Type = 
    outerType match {
      case pt: ParameterizedType => pt.getActualTypeArguments()(0)
      case _                     => throw new UnsupportedOperationException(s"Expected ParameterizedType. Found $outerType")
    }
  

  override def serialize(
      src: Option[Any],
      typeOfSrc: Type,
      context: JsonSerializationContext
  ): JsonElement = {
    src match {
      case None    => JsonNull.INSTANCE
      case Some(v) => context.serialize(v, innerType(typeOfSrc))
    }
  }

  override def deserialize(
      json: JsonElement,
      typeOfT: Type,
      context: JsonDeserializationContext
  ): Option[Any] = {
    json match {
      case null                 => None
      case _ if json.isJsonNull => None
      case _                    => Some(context.deserialize(json, innerType(typeOfT)))
    }
  }
}

// Pretend these are JPA entities. So mutable vars are required for this case
class Foo[A]() {
  var required: String = _
  var optional: Option[A] = None
  override def toString(): String = s"Foo($required, $optional)"
}
object Foo {
  def apply[A](required: String, optional: Option[A] = None): Foo[A] = {
    val foo = new Foo[A]
    foo.required = required
    foo.optional = optional
    foo
  }
}

def display(a: Foo[?], json: String, b: Foo[?]): Unit = {
  println("----------------")
  println(s"START: $a")
  println(s"JSON:  $json")
  println(s"END:   $b")
}

val gson = new GsonBuilder()
      .setPrettyPrinting()
      .registerTypeAdapter(classOf[Option[Any]], new OptionSerializer)
      .create()

val a = Foo("hello", Some(3))
val jsonA = gson.toJson(a)
val aa = gson.fromJson(jsonA, classOf[Foo[Int]])
display(a, jsonA, aa)

val b = Foo("hello", Some("world"))
val jsonB = gson.toJson(b)
val bb = gson.fromJson(jsonB, classOf[Foo[String]])
display(b, jsonB, bb)

val c = Foo("goodbye")
val jsonC = gson.toJson(c)
val cc = gson.fromJson(jsonC, classOf[Foo[String]])
display(c, jsonC, cc)
解决方案

Scala 3中Option作为泛型特质,其Java反射类型不再直接是ParameterizedType,需通过以下方式提取内部参数化类型:

兼容Scala 2/3的通用实现

通过反射访问Scala 3泛型类型的内部字段,同时保留Scala 2的兼容逻辑:

import java.lang.reflect.Type
import com.google.gson._
import java.lang.reflect.ParameterizedType

class OptionSerializer extends JsonSerializer[Option[Any]] with JsonDeserializer[Option[Any]] {

  private def innerType(outerType: Type): Type = outerType match {
    // 兼容Scala 2的ParameterizedType场景
    case pt: ParameterizedType =>
      pt.getActualTypeArguments()(0)
    // 处理Scala 3的泛型类型
    case _ =>
      try {
        // Scala 3生成的泛型类型会包含argTypes字段存储类型参数
        val argTypesField = outerType.getClass.getDeclaredField("argTypes")
        argTypesField.setAccessible(true)
        val argTypes = argTypesField.get(outerType).asInstanceOf[Array[Type]]
        if (argTypes.nonEmpty) argTypes(0)
        else throw new UnsupportedOperationException("No type arguments found for Option")
      } catch {
        case e: NoSuchFieldException =>
          throw new UnsupportedOperationException(s"Failed to extract inner type from $outerType", e)
      }
  }

  // 原serialize和deserialize方法保持不变
  override def serialize(
      src: Option[Any],
      typeOfSrc: Type,
      context: JsonSerializationContext
  ): JsonElement = {
    src match {
      case None    => JsonNull.INSTANCE
      case Some(v) => context.serialize(v, innerType(typeOfSrc))
    }
  }

  override def deserialize(
      json: JsonElement,
      typeOfT: Type,
      context: JsonDeserializationContext
  ): Option[Any] = {
    json match {
      case null                 => None
      case _ if json.isJsonNull => None
      case _                    => Some(context.deserialize(json, innerType(typeOfT)))
    }
  }
}

关键说明

  • Scala 3的泛型类型在Java反射中会被包装为带有argTypes字段的实现类,通过反射访问该字段可获取类型参数。
  • 上述实现兼容Scala 2和Scala 3,无需修改其他业务代码。
  • 若项目中使用TypeToken传递类型信息(如new TypeToken[Option[String]](){}),可直接从TypeToken.getType()获取参数化类型,逻辑会更可靠。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 23:22:05