我在Jackson streaming API的帮助下在Scala中实现了一些JSON反序列化逻辑。现在,我的代码有效,但它不是很漂亮。我希望代码更具功能性,即避免使用命令式和可变变量。
下面包含的代码片段纯粹是为了问题而构建的,但演示了我当前的反序列化逻辑,应用于示例类Container
及其类Value
的子实例。入口点是测试"可以反序列化容器",它使用JsonParserService
类来解析一些示例JSON。
如何以更加功能的方式重新编写此解析代码,没有可变变量等等?理想情况下,我认为JsonParserService.parseJson
应该能够以某种方式构造并返回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"))))
}
}
答案 0 :(得分:2)
我想出了一种技术,需要将JSON字段转换为Map[String, Any]
,用户提供的lambda用于实例化所需的类。我认为这是一个非常干净的解决方案,虽然可能有更好的方法(免责声明:我是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")))
}
}