在Spray中,倘若我们希望REST服务支持JSON格式的request与response,通常使用Spray提供的Json4sSupport
,只需要Spray的Route继承它即可。它基本上可以应付常规的Scala类(多数情况是case class)与Json格式之间的序列化与反序列化。
倘若需要支持Scala的枚举类型,或者Joda框架提供的Time类型,可以利用Json4s的扩展,只需要在项目依赖文件sbt中添加该依赖:
val json4sExt = "org.json4s" %% "json4s-ext" % json4sVersion
那么就可以定义一个类,用于对Json的支持:
object MyJsonSupport extends Json4sSupport {
implicit val formats = Serialization.formats(ShortTypeHints(List()))
implicit lazy val json4sFormats: Formats =
org.json4s.ext.JodaTimeSerializers.all +
new EnumNameSerializer(FieldType)
}
上述代码的FieldType
就是一个自定义的枚举类型。
在项目中,我碰到了一个复杂的Json结构,它是一个递归结构,且内部嵌套的对象还存在类型定义的多态。若从函数角度来讲,那种类型结构就是Algebraic Data Type的product类型。
大致的Json结构如下所示:
[
{
"fieldId": -1,
"fieldType": "metric",
"function": {
"functionName": "sum",
"functionType": "buildin",
"parameters": [
{
//此时参数为FieldParameter类型
"fieldId": 4
}
]
}
},
{
"fieldId": -1,
"fieldType": "metric",
"function": {
"functionName": "yearOnYearBasis",
"functionType": "udaf",
"parameters": [
{
"fieldId": 4
},
{
"fieldId": 5
},
{
//此时参数为ConstantParameter类型
"classType": "timestamp",
"value": "2015-01-01 00:00:00"
},
{
"classType": "timestamp",
"value": "2015-12-31 11:59:59"
}
]
}
}
]
这个Json数据代表一个Field
的数组。Field下定义了一个Option
的Function
。Function
可以接收多个参数(Parameter
),而参数存在三种类型,分别为:FieldParameter
,ConstantParameter
,FunctionParameter
。其中FunctionParameter
尤其特殊,它实际上又是一个Function
类型,形成了一种递归嵌套。
Json数据对应的Scala类如下:
case class Field(fieldId: ID, fieldType: String, function: Option[Function] = None)
case class Function(functionName: String, functionType: String, parameters: List[Parameter])
sealed trait Parameter
case class FieldParameter(fieldId: ID) extends Parameter
case class ConstantParameter(classType: String, value: String) extends Parameter
case class FunctionParameter(function: Function) extends Parameter
我们需要在给REST服务传入Json数据时,可以根据Json中parameters下传递的名称,来判断实例化哪一种类型的Parameter。在Scala中,其实是一个典型的模式匹配。
在Json4s中,可以认为Json值其实是一个它封装的JObject对象。一个JObject可以包含多个JField对象。此外,Json4s支持自定义序列化器。这就为类型多态提供了实现的可能。除了Parameter
类型外,其余类型符合标准的Scala类(即使包含了Option类型),Json4s内置的序列化器已经支持;故而只需要为Parameter
定义序列化器即可。
由于要支持序列化与反序列化,因此在模式匹配中需要支持两个方向的相互转换与映射,即在JObject与case class之间定义。
对FunctionParameter
的处理比较特殊,因为它的参数(即Json4s中的JField
)又是另外一个对象。而Json4s中仅仅为基本类型提供了定义,例如JInt
、JString
等。在JField
中的值都被定义为JValue
,所以可以通过调用JValue
的extract
方法将JValue
提取为Scala对象;通过调用Extraction.decompose
方法将Scala对象转换为JValue
对象。代码如下所示:
class ParameterSerializer extends CustomSerializer[Parameter](format => ( {
case JObject(JField("fieldId", JInt(fieldId)) :: Nil) => FieldParameter(fieldId.toInt)
case JObject(JField("classType", JString(classType)) :: JField("value", JString(value)) :: Nil) => ConstantParameter(classType, value)
case JObject(List(JField("function", function) :: Nil) => FunctionParameter(function.extract[Function])
}, {
case FieldParameter(fieldId) => JObject(JField("fieldId", JInt(fieldId)) :: Nil)
case ConstantParameter(classType, value) => JObject(JField("classType", JString(classType)) :: JField("value", JString(value)) :: Nil)
case FunctionParameter(function) => JObject(JField("function", Extraction.decompose(Function)) :: Nil)
}))
最后,只需要将ParameterSerializer
的实例追加到前面的json4sFormats
中即可。