Десериализация строки Java в класс Java с использованием Lift JSON в Scala 3

Я пытаюсь реализовать «универсальную» десериализацию в 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. Это верно? Можно ли исправить этот код без серьезного рефакторинга?

Поскольку я не знаю, что здесь происходит, я подумал о нескольких других подходах:

  • Пытался получить неявный TypeTag или Manifest (могу публиковать нерабочие фрагменты). Их необходимо явно передать со стороны Java, и я не нашел документации о том, как их создать, используя только имя класса. Это возможно?
  • Я предполагаю, что есть способ конвертировать izumi-reflectTypeTag в Scala2 Manifest-совместимые объекты, но я не знаю, как это сделать, и боюсь, что это хакерски и ненадежно. Возможен ли такой подход и имеет ли он больше смысла?
  • В качестве альтернативы я предполагаю, что могу использовать другую библиотеку Scala3 JSON, которая может десериализоваться в TypeTag, который предоставляется как (неявный?) аргумент. Есть ли библиотека Scala3, которая позволила бы мне сделать это без изменения определений классов регистров?
  • Может быть, я смогу получить манифест Scala3 другим способом, который мне не понятен? Документация Scala3 по Mirror кажется скудной. Могу ли я создать метод Scala3, который получает имя класса и возвращает кортеж java.lang.Class и Manifest? Имеет ли это вообще смысл, если в «Манифесте» нет общего аргумента?
  • AFAIU, макросы Scala3 являются предполагаемой заменой отражения Scala2. Можно/нужно ли это сделать с помощью макросов Scala3?

Я знаю, что этот вопрос требует много внимания, поэтому спасибо, что зашли так далеко :) Я изо всех сил старался предоставить как можно больше информации, не усложняя ее, но я бы с радостью поделился дополнительным контекстом - например, неработающим примером, над которым я потею.

Просто подытожу проблему:
Я пытаюсь десериализовать строку JSON в класс Scala3, который знаю только по имени. Я ожидаю, что этот класс будет классом случая. В настоящее время я пытаюсь и не могу использовать библиотеку Scala2 Lift JSON. Я хочу, чтобы классы случаев, в которые будет десериализован JSON, имели простые определения, поэтому мне не нужны методы serialize/deserialize или подобные подходы.

Как мне добиться этого в Scala 3?

«Я выбрал JSON-библиотеку Lift из-за возможности явной передачи целевого класса для десериализации» — из-за этого можно легко испортить ситуацию. Отражение во время выполнения в JVM работает после стирания типа, поэтому информация о том, что находится внутри дженериков (например, Option[Int] или Option[String]), исчезает. Судя по тому, что я вижу, Manifest используется для восстановления этих фрагментов информации, но в Scala 3 манифестов нет, и они были сгенерированы компилятором. Я бы не осмелился генерировать их вручную на стороне Java. Я бы также не стал использовать Lift для новых проектов.

Mateusz Kubuszok 17.04.2024 01:10

Если вы хотите использовать Scala 3, я бы забыл о любой библиотеке JSON, которая не основана на какой-либо кросс-скомпилированной (или только Scala 3) библиотеке — эти вещи зависят от кода, сгенерированного компилятором (обычно интерфейсы, известные как классы типов, их реализация обеспечивается некоторым библиотечным кодом на основе типа, этот процесс известен как деривация) - поскольку большинство из них в какой-то момент зависит от макросов (зеркала Scala 3 являются исключением), они тесно связаны с конкретной версией Scala. for3use2_13 не позволяет вызывать макросы Scala 2 - макросов либо нет, либо они уже развернуты.

Mateusz Kubuszok 17.04.2024 01:16

Я считаю, что Джексон (модуль Scala) уже довольно давно поддерживает Scala 3.

Gaël J 17.04.2024 07:05

@MateuszKubuszok Спасибо за комментарий! Означает ли это, что выбранный мной подход к использованию Lift JSON мертв по прибытии? Если да, то какую альтернативу вы бы порекомендовали?

K.Steff 17.04.2024 14:07

@GaëlJ Можете ли вы показать фрагмент в Scala 3 с Джексоном, который может десериализовать классы случаев, такие как приведенные выше, а также получать имя класса в качестве строкового аргумента? Если да, то напишите ответ и я с радостью его приму

K.Steff 17.04.2024 15:29

Я видел несколько билетов на странице GH Lift, где я увидел, что некоторые модули еще не переведены на 2.13 (правда, не JSON), но на Scala 3 нет ETA - пока они не перенесут свои макросы на Scala 3, да, используя Lift Библиотеки JSON в Scala 3 — это DOA. Существует множество альтернатив: Джексон (если вы не любите классы типов, но Джексон использует отражение во время выполнения, так что стирание типов снова актуально и может стать проблемой?), Circe (если вы любите кошек), Jsoniter Scala (Для генерации кодека ATM требуется ручной вызов JsonCodecMaker.make, но это быстрая вещь в JVM AFAIK), также есть PlayJSON

Mateusz Kubuszok 17.04.2024 17:44

@K.Steff, делать особо нечего. Ознакомьтесь с документацией: github.com/FasterXML/jackson-module-scala.

Gaël J 17.04.2024 19:00

@GaëlJ Возможно, я этого не вижу, но когда я смотрю на документацию jackson-scala, я не понимаю, как я могу преобразовать значение java.lang.Class в общий второй аргумент JavaTypeable[T] для readValue . Если я что-то упустил, пожалуйста, поделитесь

K.Steff 18.04.2024 01:05

Ссылка, на которую вы указали, предназначена для получения «богатых» функций преобразования. Вам должно хватить старого доброго Джексона readValue(... src, Class<T> valueType). На примере Mapper.

Gaël J 18.04.2024 07:26

@GaëlJ Как старый добрый Джексон мог справиться с Option[Int]? Я так понимаю, мне нужна библиотека Scala, иначе снова появится проблема со стиранием типов. Есть ли что-то, что мне не хватает?

K.Steff 19.04.2024 15:04

@MateuszKubuszok Можете ли вы указать пример одной из упомянутых вами библиотек, которая может десериализовать классы случаев, такие как приведенные выше, а также получать имя класса в качестве строкового аргумента? В частности, Option[Int], который AFAIU не будет работать с простой библиотекой Java, используемой из Scala.

K.Steff 19.04.2024 15:06
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
11
63
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Джексон (с модулем 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)))

Другие вопросы по теме