Я пытаюсь реализовать «универсальную» десериализацию в JSON для простых типов Scala3 (например, классов случаев) – мне нужно передать сериализованный JSON и имя (строку) целевого класса и получить экземпляр целевого класса ( что я впоследствии рассматриваю как java.lang.Object
).
Я решил использовать библиотеку JSON платформы Lift. Поскольку Lift предназначен только для Scala2, я использую его через уровень совместимости CrossVersion.for3Use2_13 (это из моего build.sbt):libraryDependencies += ("net.liftweb" %% "lift-json" % "3.5.0").cross(CrossVersion.for3Use2_13),
Я выбрал JSON-библиотеку Lift из-за возможности явной передачи целевого класса для десериализации (см. документацию Extraction Lift):def extract(json: JValue, target: TypeInfo)(implicit formats: Formats): Any
Чтобы использовать это, я сначала «разыменовываю» имя класса на экземпляр java.lang.Class в Java:
String targetClassName = (inp.entrypointClass == null ? "Main" : inp.entrypointClass);
String mainClassName = this.getClass().getPackage().getName() + "." + targetClassName;
Class targetClass;
try {
targetClass = Class.forName(mainClassName);
} catch(ClassNotFoundException err1) {
throw new InvalidEntrypointClassError(err1);
}
Кажется, эта часть работает правильно (о чем свидетельствует тот факт, что я не получаю InvalidEntrypointClassError
при попытках отладки).
Затем я пытаюсь десериализовать строку в класс следующим образом:
import net.liftweb.json._
import net.liftweb.json.Extraction._
import net.liftweb.json.Serialization._
import reflect._
val formats = net.liftweb.json.DefaultFormats
object ScalaWrapper {
def deserialize(jsonData: java.lang.String, clazz: java.lang.Class[?]): java.lang.Object = {
val typeInfo = TypeInfo(clazz, None)
val data = parse(jsonData)
val res: Any = extract(data, typeInfo)(formats)
res.asInstanceOf[java.lang.Object]
}
}
Это не удается, когда я пробую это на простых примерах, таких как попытка десериализации в класс случая Input
, определенный здесь:
case class Node(name: String, left: Option[Int], right: Option[Int])
case class Walk(nodes: List[Node])
case class Input(nodes: List[Node])
Обратите внимание, что для меня важно иметь «простые» определения классов случаев, то есть передача сложности проблемы десериализации классам случаев НЕ является решением из-за других требований.
со следующим вводом JSON:
{
"nodes": [
{
"name": "A",
"left": 1,
"right": 2
},
{
"name": "B"
},
{
"name": "C",
"left": 4,
"right": 3
},
{
"name": "D"
},
{
"name": "E"
}
]
}
Ошибка находится довольно глубоко внутри Lift JSON:
java.lang.NullPointerException
at scala.tools.scalap.scalax.rules.scalasig.ByteCode$.forClass(ClassFileParser.scala:24)
at scala.tools.scalap.scalax.rules.scalasig.ScalaSigParser$.parse(ScalaSig.scala:68)
at net.liftweb.json.ScalaSigReader$.findScalaSig(ScalaSig.scala:126)
at net.liftweb.json.ScalaSigReader$.$anonfun$findScalaSig$1(ScalaSig.scala:126)
at scala.Option.orElse(Option.scala:477)
at net.liftweb.json.ScalaSigReader$.findScalaSig(ScalaSig.scala:126)
at net.liftweb.json.ScalaSigReader$.findClass(ScalaSig.scala:60)
at net.liftweb.json.ScalaSigReader$.readConstructor(ScalaSig.scala:44)
... <SKIPPED ABOUT 20 OTHER STACK FRAMES>
at net.liftweb.json.Meta$.mappingOf(Meta.scala:195)
at net.liftweb.json.Extraction$.extract(Extraction.scala:210)
at binary_tree_dfs.ScalaWrapper$.deserialize(ScalaWrapper.scala:13)
at binary_tree_dfs.ScalaWrapper.deserialize(ScalaWrapper.scala)
Мне не удалось отладить это, и я не знаю Scala изнутри, чтобы по-настоящему понять, что происходит, но я предполагаю, что я расплачиваюсь за попытку использовать универсальные функции Scala2 в проекте Scala3. Это верно? Можно ли исправить этот код без серьезного рефакторинга?
Поскольку я не знаю, что здесь происходит, я подумал о нескольких других подходах:
izumi-reflect
TypeTag
в Scala2 Manifest
-совместимые объекты, но я не знаю, как это сделать, и боюсь, что это хакерски и ненадежно. Возможен ли такой подход и имеет ли он больше смысла?TypeTag
, который предоставляется как (неявный?) аргумент. Есть ли библиотека Scala3, которая позволила бы мне сделать это без изменения определений классов регистров?java.lang.Class
и Manifest
? Имеет ли это вообще смысл, если в «Манифесте» нет общего аргумента?Я знаю, что этот вопрос требует много внимания, поэтому спасибо, что зашли так далеко :) Я изо всех сил старался предоставить как можно больше информации, не усложняя ее, но я бы с радостью поделился дополнительным контекстом - например, неработающим примером, над которым я потею.
Просто подытожу проблему:
Я пытаюсь десериализовать строку JSON в класс Scala3, который знаю только по имени.
Я ожидаю, что этот класс будет классом случая. В настоящее время я пытаюсь и не могу использовать библиотеку Scala2 Lift JSON.
Я хочу, чтобы классы случаев, в которые будет десериализован JSON, имели простые определения, поэтому мне не нужны методы serialize
/deserialize
или подобные подходы.
Как мне добиться этого в Scala 3?
Если вы хотите использовать Scala 3, я бы забыл о любой библиотеке JSON, которая не основана на какой-либо кросс-скомпилированной (или только Scala 3) библиотеке — эти вещи зависят от кода, сгенерированного компилятором (обычно интерфейсы, известные как классы типов, их реализация обеспечивается некоторым библиотечным кодом на основе типа, этот процесс известен как деривация) - поскольку большинство из них в какой-то момент зависит от макросов (зеркала Scala 3 являются исключением), они тесно связаны с конкретной версией Scala. for3use2_13
не позволяет вызывать макросы Scala 2 - макросов либо нет, либо они уже развернуты.
Я считаю, что Джексон (модуль Scala) уже довольно давно поддерживает Scala 3.
@MateuszKubuszok Спасибо за комментарий! Означает ли это, что выбранный мной подход к использованию Lift JSON мертв по прибытии? Если да, то какую альтернативу вы бы порекомендовали?
@GaëlJ Можете ли вы показать фрагмент в Scala 3 с Джексоном, который может десериализовать классы случаев, такие как приведенные выше, а также получать имя класса в качестве строкового аргумента? Если да, то напишите ответ и я с радостью его приму
Я видел несколько билетов на странице GH Lift, где я увидел, что некоторые модули еще не переведены на 2.13 (правда, не JSON), но на Scala 3 нет ETA - пока они не перенесут свои макросы на Scala 3, да, используя Lift Библиотеки JSON в Scala 3 — это DOA. Существует множество альтернатив: Джексон (если вы не любите классы типов, но Джексон использует отражение во время выполнения, так что стирание типов снова актуально и может стать проблемой?), Circe (если вы любите кошек), Jsoniter Scala (Для генерации кодека ATM требуется ручной вызов JsonCodecMaker.make
, но это быстрая вещь в JVM AFAIK), также есть PlayJSON
@K.Steff, делать особо нечего. Ознакомьтесь с документацией: github.com/FasterXML/jackson-module-scala.
@GaëlJ Возможно, я этого не вижу, но когда я смотрю на документацию jackson-scala, я не понимаю, как я могу преобразовать значение java.lang.Class в общий второй аргумент JavaTypeable[T] для readValue . Если я что-то упустил, пожалуйста, поделитесь
Ссылка, на которую вы указали, предназначена для получения «богатых» функций преобразования. Вам должно хватить старого доброго Джексона readValue(... src, Class<T> valueType)
. На примере Mapper
.
@GaëlJ Как старый добрый Джексон мог справиться с Option[Int]
? Я так понимаю, мне нужна библиотека Scala, иначе снова появится проблема со стиранием типов. Есть ли что-то, что мне не хватает?
@MateuszKubuszok Можете ли вы указать пример одной из упомянутых вами библиотек, которая может десериализовать классы случаев, такие как приведенные выше, а также получать имя класса в качестве строкового аргумента? В частности, Option[Int]
, который AFAIU не будет работать с простой библиотекой Java, используемой из Scala.
Джексон (с модулем Scala) может делать то, что вы хотите (т. е. анализировать класс, известный во время выполнения).
package com.myapp
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.scala.{ClassTagExtensions, DefaultScalaModule}
case class Node(name: String, left: Option[Int], right: Option[Int])
case class Walk(nodes: List[Node])
case class Input(nodes: List[Node])
val objectMapper = JsonMapper.builder().addModule(DefaultScalaModule).build() :: ClassTagExtensions
val input: String = ???
val parsedInput = objectMapper.readValue(input, Class.forName("com.myapp.Input"))
println(parsedInput)
// Input(List(Node(A,Some(1),Some(2)), Node(B,None,None), Node(C,Some(4),Some(3)), Node(D,None,None), Node(E,None,None)))
«Я выбрал JSON-библиотеку Lift из-за возможности явной передачи целевого класса для десериализации» — из-за этого можно легко испортить ситуацию. Отражение во время выполнения в JVM работает после стирания типа, поэтому информация о том, что находится внутри дженериков (например,
Option[Int]
илиOption[String]
), исчезает. Судя по тому, что я вижу,Manifest
используется для восстановления этих фрагментов информации, но в Scala 3 манифестов нет, и они были сгенерированы компилятором. Я бы не осмелился генерировать их вручную на стороне Java. Я бы также не стал использовать Lift для новых проектов.