向Scala枚举添加无arg构造函数

时间:2017-03-14 13:08:21

标签: json scala enums gson

我有以下Scala枚举:

object RunMode extends Enumeration {
  val CLIENT_MODE = Value("CLIENT")
  val SERVER_MODE = Value("SERVER")
}

我有一些JSON我的应用程序作为输入接受,例如:

{
    "version" : "0.1",
    "runMode" : "CLIENT"
}

此处JSON字段“runMode”实际上是我的RunMode枚举,其值始终为“CLIENT”或“SERVER”。我正在尝试使用GSON将此JSON反序列化为AppConfig实例:

class AppConfig(version : String, runMode : RunMode) {
  def version() : String = { this.version }
  def runMode() : RunMode.Value = { this.runMode }
}  

我有以下GSON代码:

val gson = new Gson()
val text = Source.fromFile(jsonConfigFile).mkString
gson.fromJson(text, classOf[AppConfig])

运行时:

java.lang.RuntimeException: Unable to invoke no-args constructor for class scala.Enumeration$Value. Register an InstanceCreator with Gson for this type may fix this problem.
> Buildiat com.google.gson.internal.ConstructorConstructor$14.construct(ConstructorConstructor.java:226)
    at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:210)
    at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:129)
    at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:220)
    at com.google.gson.Gson.fromJson(Gson.java:887)
  <rest of stacktrace omitted for brevity>

很明显,GSON希望RunMode有一个no-arg构造函数,但它没有,因此它无法在运行时反序列化我的JSON文件。

我已经尝试了一百万个不同的组合,但似乎无法找到神奇的构造函数定义。所以我问:如何向RunMode添加无参数构造函数,以便GSON可以将其反序列化为AppConfig实例?

2 个答案:

答案 0 :(得分:0)

这并没有直接回答为什么使用Gson失败,但提供了另一种选择。以下是使用argonaut的示例:

RunMode枚举定义:

object RunMode extends Enumeration {
  type RunMode = Value
  val CLIENT_MODE = Value("CLIENT")
  val SERVER_MODE = Value("SERVER")

  implicit def runModeCodec: CodecJson[RunMode.RunMode] = CodecJson({
    case CLIENT_MODE => "CLIENT".asJson
    case SERVER_MODE => "SERVER".asJson
  }, c => c.focus.string match {
    case Some("CLIENT") => DecodeResult.ok(CLIENT_MODE)
    case Some("SERVER") => DecodeResult.ok(SERVER_MODE)
    case _ => DecodeResult.fail("Could not decode RunMode", c.history)
  })
}

Foo的定义(匹配您要创建的对象):

case class Foo(version: String, runMode: RunMode)
object Foo {
  implicit def codec: CodecJson[Foo] = 
    casecodec2(Foo.apply, Foo.unapply)("version", "runMode")
}

现在解码/编码示例:

object ArgonautEnumCodec {
  def main(args: Array[String]): Unit = {
    val res: String = Foo("0.1", RunMode.CLIENT_MODE).asJson.toString
    println(res)

    val foo: Foo = res.decodeOption[Foo].get
    println(foo)
  }
}

收率:

{"version":"0.1","runMode":"CLIENT"}
Foo(0.1,CLIENT)

答案 1 :(得分:0)

由于我不是Scala人,但有一些Gson背景,偷看Scala如何工作的一些见解对我来说很有趣。您获得异常的原因是Gson无法实例化抽象类scala.Enumeration.ValueAutoConfig类内容与vanilla Java中的以下类非常相似:

final class AppConfig {

    final String version;

    // This is where ig gets failed 
    final scala.Enumeration.Value runMode;

    AppConfig(final String version, final scala.Enumeration.Value runMode) {
        this.version = version;
        this.runMode = runMode;
    }

}

据我所知,Scala枚举是如何实现的,与Java枚举不同,它们本身没有类型,并且每个Scala枚举值似乎都是scala.Enumeration$Val的实例,没有提供足够的“主机”枚举从其类型中键入信息(但实例似乎有其外部类引用)。这就是为什么定制实现自定义类型适配器并不那么简单,需要对真实的枚举类型进行一些检查(不知道如何实现它)。

Gson提供了一个特殊的注释@JsonAdapter,可以注释某个字段,包括要应用的类型适配器。因此,上述类中的AppConfig.runMode可以注释为:

@JsonAdapter(RunModeEnumTypeAdapter.class)
final scala.Enumeration.Value runMode;

请注意,它在名称中对目标类型有一些提示。这是因为可能没有其他方法来指定目标枚举类型。现在,如何实现通用的scala.Enumeration类型适配器。

// E - a special generic type bound to associate a Scala enumeration with
// So any Scala enumeration can be processed with this type adapter
abstract class AbstractScalaEnumTypeAdapter<E extends scala.Enumeration>
        extends TypeAdapter<scala.Enumeration.Value> {

    private final E enumeration;

    protected AbstractScalaEnumTypeAdapter(final E enumeration) {
        this.enumeration = enumeration;
    }

    @Override
    @SuppressWarnings("resource")
    public final void write(final JsonWriter out, final scala.Enumeration.Value value)
            throws IOException {
        // If the given value is null, null must be written to the writer (however it depends on a particular Gson instance configuration)
        if ( value == null ) {
            out.nullValue();
        } else {
            // Does Scala provide something like java.lang.Enumeration#name?
            out.value(value.toString());
        }
    }

    @Override
    public final scala.Enumeration.Value read(final JsonReader in)
            throws IOException {
        final JsonToken token = in.peek();
        switch ( token ) {
        case NULL:
            // Consume the `null` JSON token
            in.nextNull();
            return null;
        case STRING:
            // Consume a JSON string value and lookup an appropriate Scala enumeration value by its name
            final String rawValue = in.nextString();
            return enumeration.withName(rawValue);
        // These case labels are matter of style and cover the rest of possible Gson JSON tokens, and are not really necessary
        case BEGIN_ARRAY:
        case END_ARRAY:
        case BEGIN_OBJECT:
        case END_OBJECT:
        case NAME:
        case NUMBER:
        case BOOLEAN:
        case END_DOCUMENT:
            throw new MalformedJsonException("Unexpected token: " + token);
        // Something else? Must never happen
        default:
            throw new AssertionError(token);
        }
    }

}

现在,RunMode可以绑定到上面的类型适配器:

final class RunModeEnumTypeAdapter
        extends AbstractScalaEnumTypeAdapter<RunMode$> {

    // Gson can instantiate this itself
    private RunModeEnumTypeAdapter() {
        // This is how it looks like from the Java perspective
        // And this is the "hint" I was talking about above
        super(RunMode$.MODULE$);
    }

}

使用示例:

final Gson gson = new Gson();
final AppConfig appConfig = gson.fromJson("{\"version\":\"0.1\",\"runMode\":\"CLIENT\"}", AppConfig.class);
System.out.println(appConfig.version);
System.out.println(appConfig.runMode);
System.out.println(gson.toJson(appConfig));

输出:

  

0.1
  CLIENT
  {“version”:“0.1”,“runMode”:“CLIENT”}

可能不像Scala那样漂亮和紧凑,但我希望上面的代码可以毫无问题地翻译成Scala。