aeson 可以处理 JSON 不精确的类型吗?
Can aeson handle JSON with imprecise types?
我必须处理 JSON 来自有时给我 "123"
而不是 123
作为字段值的服务。当然这很丑陋,但我不能改变服务。有没有一种简单的方法可以派生 FromJSON
的实例来处理这个问题?通过 deriveJSON
(https://hackage.haskell.org/package/aeson-1.5.4.1/docs/Data-Aeson-TH.html) 派生的标准实例无法做到这一点。
一个简单的(虽然可能不是那么优雅)选项是将 属性 定义为 Aeson Value
。这是一个例子:
{-#LANGUAGE DeriveGeneric #-}
module Q65410397 where
import GHC.Generics
import Data.Aeson
data JExample = JExample { jproperty :: Value } deriving (Eq, Show, Generic)
instance ToJSON JExample where
instance FromJSON JExample where
Aeson 可以用数字解码 JSON 值:
*Q65410397> decode "{\"jproperty\":123}" :: Maybe JExample
Just (JExample {jproperty = Number 123.0})
如果值为字符串,它也有效:
*Q65410397> decode "{\"jproperty\":\"123\"}" :: Maybe JExample
Just (JExample {jproperty = String "123"})
当然,通过将 属性 定义为 Value
这意味着在 Haskell 一侧,它还可以容纳数组和其他对象,因此您至少应该有一个路径在处理它的代码中。如果您绝对确定第三方服务永远不会在那个地方给您一个数组,那么以上并不是最优雅的解决方案。
另一方面,如果它给你 123
和 "123"
,已经有一些证据表明你可能不应该相信合同的类型是正确的...
假设您想尽可能避免手动编写 FromJSON
实例,也许您可以使用手工制作的 FromJSON
实例在 Int
上定义一个新类型——仅用于处理奇怪解析的字段:
{-# LANGUAGE TypeApplications #-}
import Control.Applicative
import Data.Aeson
import Data.Text
import Data.Text.Read (decimal)
newtype SpecialInt = SpecialInt { getSpecialInt :: Int } deriving (Show, Eq, Ord)
instance FromJSON SpecialInt where
parseJSON v =
let fromInt = parseJSON @Int v
fromStr = do
str <- parseJSON @Text v
case decimal str of
Right (i, _) -> pure i
Left errmsg -> fail errmsg
in SpecialInt <$> (fromInt <|> fromStr)
然后您可以为具有 SpecialInt
作为字段的记录派生 FromJSON
。
将字段设为 SpecialInt
而不是 Int
只是为了 FromJSON
实例感觉有点干扰。 “需要以一种奇怪的方式进行解析”是 属性 外部格式,而不是域。
为了避免这种尴尬并保持我们的域类型干净,我们需要一种方法来告诉 GHC:“嘿,当为我的域类型派生 FromJSON
实例时,请将此字段视为是 SpecialInt
,但 return 最后是 Int
”。也就是说,我们只想在反序列化时处理 SpecialInt
。这可以使用 "generic-data-surgery" 库来完成。
考虑这种类型
{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics
data User = User { name :: String, age :: Int } deriving (Show,Generic)
并假设我们想要将“年龄”解析为 SpecialInt
。我们可以这样做:
{-# LANGUAGE DataKinds #-}
import Generic.Data.Surgery (toOR', modifyRField, fromOR, Data)
instance FromJSON User where
parseJSON v = do
r <- genericParseJSON defaultOptions v
-- r is a synthetic Data which we must tweak in the OR and convert to User
let surgery = fromOR . modifyRField @"age" @1 getSpecialInt . toOR'
pure (surgery r)
投入使用:
{-# LANGUAGE OverloadedStrings #-}
main :: IO ()
main = do
print $ eitherDecode' @User $ "{ \"name\" : \"John\", \"age\" : \"123\" }"
print $ eitherDecode' @User $ "{ \"name\" : \"John\", \"age\" : 123 }"
一个限制是“generic-data-surgery”通过调整 Generic
representations, so this technique won't work with deserializers generated using Template Haskell.
来工作
我必须处理 JSON 来自有时给我 "123"
而不是 123
作为字段值的服务。当然这很丑陋,但我不能改变服务。有没有一种简单的方法可以派生 FromJSON
的实例来处理这个问题?通过 deriveJSON
(https://hackage.haskell.org/package/aeson-1.5.4.1/docs/Data-Aeson-TH.html) 派生的标准实例无法做到这一点。
一个简单的(虽然可能不是那么优雅)选项是将 属性 定义为 Aeson Value
。这是一个例子:
{-#LANGUAGE DeriveGeneric #-}
module Q65410397 where
import GHC.Generics
import Data.Aeson
data JExample = JExample { jproperty :: Value } deriving (Eq, Show, Generic)
instance ToJSON JExample where
instance FromJSON JExample where
Aeson 可以用数字解码 JSON 值:
*Q65410397> decode "{\"jproperty\":123}" :: Maybe JExample
Just (JExample {jproperty = Number 123.0})
如果值为字符串,它也有效:
*Q65410397> decode "{\"jproperty\":\"123\"}" :: Maybe JExample
Just (JExample {jproperty = String "123"})
当然,通过将 属性 定义为 Value
这意味着在 Haskell 一侧,它还可以容纳数组和其他对象,因此您至少应该有一个路径在处理它的代码中。如果您绝对确定第三方服务永远不会在那个地方给您一个数组,那么以上并不是最优雅的解决方案。
另一方面,如果它给你 123
和 "123"
,已经有一些证据表明你可能不应该相信合同的类型是正确的...
假设您想尽可能避免手动编写 FromJSON
实例,也许您可以使用手工制作的 FromJSON
实例在 Int
上定义一个新类型——仅用于处理奇怪解析的字段:
{-# LANGUAGE TypeApplications #-}
import Control.Applicative
import Data.Aeson
import Data.Text
import Data.Text.Read (decimal)
newtype SpecialInt = SpecialInt { getSpecialInt :: Int } deriving (Show, Eq, Ord)
instance FromJSON SpecialInt where
parseJSON v =
let fromInt = parseJSON @Int v
fromStr = do
str <- parseJSON @Text v
case decimal str of
Right (i, _) -> pure i
Left errmsg -> fail errmsg
in SpecialInt <$> (fromInt <|> fromStr)
然后您可以为具有 SpecialInt
作为字段的记录派生 FromJSON
。
将字段设为 SpecialInt
而不是 Int
只是为了 FromJSON
实例感觉有点干扰。 “需要以一种奇怪的方式进行解析”是 属性 外部格式,而不是域。
为了避免这种尴尬并保持我们的域类型干净,我们需要一种方法来告诉 GHC:“嘿,当为我的域类型派生 FromJSON
实例时,请将此字段视为是 SpecialInt
,但 return 最后是 Int
”。也就是说,我们只想在反序列化时处理 SpecialInt
。这可以使用 "generic-data-surgery" 库来完成。
考虑这种类型
{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics
data User = User { name :: String, age :: Int } deriving (Show,Generic)
并假设我们想要将“年龄”解析为 SpecialInt
。我们可以这样做:
{-# LANGUAGE DataKinds #-}
import Generic.Data.Surgery (toOR', modifyRField, fromOR, Data)
instance FromJSON User where
parseJSON v = do
r <- genericParseJSON defaultOptions v
-- r is a synthetic Data which we must tweak in the OR and convert to User
let surgery = fromOR . modifyRField @"age" @1 getSpecialInt . toOR'
pure (surgery r)
投入使用:
{-# LANGUAGE OverloadedStrings #-}
main :: IO ()
main = do
print $ eitherDecode' @User $ "{ \"name\" : \"John\", \"age\" : \"123\" }"
print $ eitherDecode' @User $ "{ \"name\" : \"John\", \"age\" : 123 }"
一个限制是“generic-data-surgery”通过调整 Generic
representations, so this technique won't work with deserializers generated using Template Haskell.