假设您想尽可能避免手动编写 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 实例时,请将此字段视为 @987654335 @,但最后返回一个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”通过调整Genericrepresentations 起作用,因此该技术不适用于使用Template Haskell 生成的反序列化器。