如何以适当的功能风格编写杰克逊反序列化逻辑?

时间:2014-06-28 14:06:48

标签: json scala functional-programming jackson

我在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"))))
  }
}

1 个答案:

答案 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")))
  }
}
相关问题