Как написать эту логику десериализации Джексона в надлежащем функциональном стиле?

#json #scala #functional-programming #jackson

#json #scala #функциональное программирование #джексон

Вопрос:

Я реализовал некоторую логику десериализации JSON в Scala с помощью потокового API Jackson. Теперь мой код работает, но он не очень красивый. Я хотел бы, чтобы код был более функциональным, т. Е. Избегал императивности и изменяемых переменных.

Приведенный ниже фрагмент кода создан исключительно ради вопроса, но демонстрирует мою текущую логику десериализации, применяемую к классу example Container и его дочерним экземплярам class Value . Точкой входа является тест «Может десериализовать контейнер», который использует JsonParserService класс для анализа некоторого примера JSON.

Как я могу переписать этот код синтаксического анализа в более функциональном стиле, без изменяемых переменных и так далее? В идеале, я полагаю, что JsonParserService.parseJson это должно каким-то образом иметь возможность создавать и возвращать a Container в общем виде, без специальных знаний об этом классе (или любых других модельных классах, если на то пошло).

Дайте мне знать, если мне нужно предоставить больше информации.

 import org.scalatest.{Matchers, FunSuite}
import java.io.{InputStream, ByteArrayInputStream}
import java.nio.charset.StandardCharsets
import com.fasterxml.jackson.core.{JsonToken, JsonParser, JsonFactory}

case class ValueId(value: String)

case class Value(id: ValueId, name: String)

case class Container(values: Seq[Value])

object JsonParserService {
  def parseJson(is: InputStream, parseField: (JsonParser, String) => Unit): Unit = {
    val json = io.Source.fromInputStream(is).getLines().mkString("n")
    val parser = new JsonFactory().createParser(json)
    try {
      // Get START_OBJECT
      parser.nextToken()
      parseObject(parser, parseField)
    }
    finally {
      parser.close()
    }
  }

  def parseObject(parser: JsonParser, parseField: (JsonParser, String) => Unit): Unit = {
    assert (parser.getCurrentToken == JsonToken.START_OBJECT)
    // Read field name or END_OBJECT
    while (parser.nextToken() != JsonToken.END_OBJECT) {
      assert (parser.getCurrentToken == JsonToken.FIELD_NAME)
      val fieldName = parser.getCurrentName
      // Read value, or START_OBJECT/START_ARRAY
      parser.nextToken()
      parseField(parser, fieldName)
    }
  }
}

class JsonParserServiceTest extends FunSuite with Matchers {
  test("Can deserialize container") {
    val stream = new ByteArrayInputStream(
      """{
        | "values": [
        |   {
        |     "id": "1",
        |     "name": "name"
        |   }
        | ]
        |}""".stripMargin.getBytes(StandardCharsets.UTF_8))
    var values = Seq.empty[Value]
    var gotContainer: Option[Container] = None
    JsonParserService.parseJson(stream, {(parser, fieldName) =>
      fieldName match {
        case "values" =>
          assert (parser.getCurrentToken == JsonToken.START_ARRAY)

          // Read contents of array
          val array = collection.mutable.Buffer[Value]()
          while (parser.nextToken() != JsonToken.END_ARRAY) {
            var id: Option[ValueId] = None
            var name: Option[String] = None
            JsonParserService.parseObject(parser, {(parser, fieldName) =>
              fieldName match {
                case "id" => id = Some(ValueId(parser.getValueAsString()))
                case "name" => name = Some(parser.getValueAsString())
              }
            })
            array  = Value(id.get, name.get)
          }

          values = array.toSeq
      }

      gotContainer = Some(Container(values))
    })
    gotContainer shouldEqual Some(Container(Seq(Value(ValueId("1"), "name"))))
  }
}
 

Ответ №1:

Я придумал метод, который включает в себя преобразование полей JSON в a Map[String, Any] , которые пользовательская лямбда-формула использует для создания экземпляра требуемого класса. Я думаю, что это довольно чистое решение, хотя могут быть и лучшие способы (отказ от ответственности: Я новичок в Scala):

 import org.scalatest.{Matchers, FunSuite}
import java.io.{InputStream, ByteArrayInputStream}
import java.nio.charset.StandardCharsets
import com.fasterxml.jackson.core.{JsonToken, JsonParser, JsonFactory}
import scala.collection.mutable

case class ValueId(value: String)

case class Value(id: ValueId, name: String)

case class Container(values: Seq[Value])

case class ValueMap(map: mutable.Map[String, Any] = mutable.Map.empty[String, Any]) {
  def add(key: String, value: Any): Unit = map(key) = value

  def get[T](key: String): T = map(key).asInstanceOf[T]
}

object JsonParserService {
  def parseJson[T](is: InputStream, field2converter: Map[String, (JsonParser) => Any],
                   constructor: ValueMap => T): T = {
    val json = io.Source.fromInputStream(is).getLines().mkString("n")
    val parser = new JsonFactory().createParser(json)
    try {
      // Get START_OBJECT
      parser.nextToken()
      parseObject(parser, field2converter, constructor)
    }
    finally {
      parser.close()
    }
  }

  def parseObject[T](parser: JsonParser, field2converter: Map[String, JsonParser => Any],
                     constructor: ValueMap => T): T = {
    assert(parser.getCurrentToken == JsonToken.START_OBJECT)
    val valueMap = ValueMap()
    // Read field name or END_OBJECT
    while (parser.nextToken() != JsonToken.END_OBJECT) {
      assert(parser.getCurrentToken == JsonToken.FIELD_NAME)
      val fieldName = parser.getCurrentName
      // Read value, or START_OBJECT/START_ARRAY
      parser.nextToken()

      valueMap.add(fieldName, field2converter(fieldName)(parser))
    }

    constructor(valueMap)
  }

  def parseSeq[T](parser: JsonParser, converter: (JsonParser) => T): Seq[T] = {
    assert(parser.getCurrentToken == JsonToken.START_ARRAY)

    // Read contents of array
    val array = collection.mutable.Buffer[T]()
    while (parser.nextToken() != JsonToken.END_ARRAY) {
      array  = converter(parser)
    }
    array.toSeq
  }

  def parseString(parser: JsonParser): String = parser.getValueAsString
}

class JsonParserServiceTest extends FunSuite with Matchers {
  test("Can deserialize container") {
    val stream = new ByteArrayInputStream(
      """{
        | "values": [
        |   {
        |     "id": "1",
        |     "name": "name"
        |   }
        | ]
        |}""".stripMargin.getBytes(StandardCharsets.UTF_8))

    val gotContainer = JsonParserService.parseJson(stream, Map(("values",
      JsonParserService.parseSeq(_, JsonParserService.parseObject(_, Map(
        ("id", JsonParserService.parseString _),
        ("name", JsonParserService.parseString _)
      ), valueMap => Value(ValueId(valueMap.get[String]("id")), valueMap.get[String]("name")))
      ))),
      (valueMap) => Container(valueMap.get[Seq[Value]]("values")))

    gotContainer shouldEqual Container(Seq(Value(ValueId("1"), "name")))
  }
}