#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")))
}
}