如何处理Haskell记录字段中的保留关键字?
How to deal with Haskell's reserved keywords in record fields?
JSON 响应 Github Gists Rest API contains Haskell's keyword type
。但是type
不能作为记录字段。
因此它不能用于 Aeson's Generic FromJSON/ToJSON 个实例的实现。
import Data.Text (Text)
import GHC.Generics (Generic)
type URL = Text
data OwnerType = User deriving (Show)
data Owner = Owner {
id :: Int,
gravatar_id :: Text,
login :: Text,
avatar_url :: Text,
events_url :: URL,
followers_url :: URL,
following_url :: URL,
gists_url :: URL,
html_url :: URL,
organizations_url :: URL,
received_events_url :: URL,
repos_url :: URL,
starred_url :: URL,
subscriptions_url :: URL,
url :: URL,
-- type :: Text,
site_admin :: Bool
} deriving (Generic, Show)
instance ToJSON Owner
instance FromJSON Owner
问题:
有没有合适的方法来处理这种冲突?
我们可以使用 TemplateHaskell
来解决这个问题。我们可以使用键的特定映射,而不是写 ToJSON
和 FromJON
。
首先我们要为字段构造一个非类型的名称,例如:
data Owner = Owner {
id :: Int,
gravatar_id :: Text,
login :: Text,
avatar_url :: Text,
events_url :: URL,
followers_url :: URL,
following_url :: URL,
gists_url :: URL,
html_url :: URL,
organizations_url :: URL,
received_events_url :: URL,
repos_url :: URL,
starred_url :: URL,
subscriptions_url :: URL,
url :: URL,
<b>owner_type</b> :: Text,
site_admin :: Bool
} deriving (Generic, Show)
现在我们可以使用 deriveJSON :: Options -> Name -> Q [Dec]
函数构造一个 fromJSON
和 toJSON
实例。
这里的关键是Options
参数:它包含一个fieldLabelModifier :: String -> String
字段,可以将字段的名称重写为[=中的键74=]。因此,我们可以在这里生成一个函数来重写它。
所以我们先构造一个函数ownerFieldRename :: String -> String
:
ownerFieldRename :: String -> String
ownerFieldRename "owner_type" = "type"
ownerFieldRename name = name
所以这个函数作为恒等函数,除了"owner_type"
,映射到"type"
.
现在我们可以使用自定义选项调用 deriveJSON
函数,例如:
$(deriveJSON defaultOptions {fieldLabelModifier = ownerFieldRename} ''Owner)
或完整:
RenameUtils.hs
:
<b>module RenameUtils where
ownerFieldRename :: String -> String
ownerFieldRename "owner_type" = "type"
ownerFieldRename name = name</b>
MainFile.hs
:
<b>{-# LANGUAGE TemplateHaskell #-}</b>
{-# LANGUAGE DeriveGeneric #-}
<b>import Data.Aeson.TH(deriveJSON, defaultOptions, Options(fieldLabelModifier))</b>
<b>import RenameUtils(ownerFieldRename)</b>
import Data.Text (Text)
type URL = Text
data Owner = Owner {
id :: Int,
gravatar_id :: Text,
login :: Text,
avatar_url :: Text,
events_url :: URL,
followers_url :: URL,
following_url :: URL,
gists_url :: URL,
html_url :: URL,
organizations_url :: URL,
received_events_url :: URL,
repos_url :: URL,
starred_url :: URL,
subscriptions_url :: URL,
url :: URL,
<b>owner_type</b> :: Text,
site_admin :: Bool
} deriving (Show)
<b>$(deriveJSON defaultOptions {fieldLabelModifier = ownerFieldRename} ''Owner)</b>
现在我们得到一个 JSON 对象,以 type
为键:
Prelude Main Data.Aeson> encode (Owner 1 "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" True)
"{\"id\":1,\"gravatar_id\":\"\",\"login\":\"\",\"avatar_url\":\"\",\"events_url\":\"\",\"followers_url\":\"\",\"following_url\":\"\",\"gists_url\":\"\",\"html_url\":\"\",\"organizations_url\":\"\",\"received_events_url\":\"\",\"repos_url\":\"\",\"starred_url\":\"\",\"subscriptions_url\":\"\",\"url\":\"\",\"type\":\"\",\"site_admin\":true}"
对于一个简单的fieldLabelModifier
函数我们不需要写一个特定的函数(我们必须在特定的模块中定义),我们也可以使用一个lambda表达式 这里:
MainFile.hs
:
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveGeneric #-}
import Data.Aeson.TH(deriveJSON, defaultOptions, Options(fieldLabelModifier))
import Data.Text (Text)
type URL = Text
data Owner = Owner {
id :: Int,
gravatar_id :: Text,
login :: Text,
avatar_url :: Text,
events_url :: URL,
followers_url :: URL,
following_url :: URL,
gists_url :: URL,
html_url :: URL,
organizations_url :: URL,
received_events_url :: URL,
repos_url :: URL,
starred_url :: URL,
subscriptions_url :: URL,
url :: URL,
<b>owner_type</b> :: Text,
site_admin :: Bool
} deriving (Show)
$(deriveJSON defaultOptions {fieldLabelModifier = <b>\x -> if x == "owner_type" then "type" else x</b>} ''Owner)
Willem 的答案可能更合适,可能更符合你的要求,但这里有另一种方法,允许你定义不冲突的数据,而不必为其编写 ToJSON 和 FromJSON 实例,定义类型
data OwnerData = OwnerData {
oid :: Int
-- ... other data with non-conflicting names
} deriving (Show, Generic)
和
data Owner = Owner {
owner_data :: OwnerData,
user_type :: Text
} deriving (Show)
我们现在可以定义以下实例:
-- nothing special for OwnerData:
instance ToJSON OwnerData
instance FromJSON OwnerData
-- a little helper function to extract the hashmap(Object) from a value
toObject :: ToJSON a => a -> Object
toObject a = case toJSON a of
Object o -> o
_ -> error "toObject: value isn't an Object"
-- the instances for Owner
instance ToJSON Owner where
toJSON (Owner {owner_data = ownerData, user_type = userType}) =
Object $
toObject ownerData <> HML.fromList ["type" .= userType]
toEncoding (Owner {owner_data = ownerData, user_type = userType}) =
pairs . foldMap (uncurry (.=)) . HML.toList $
toObject ownerData <> HML.fromList ["type" .= userType]
instance FromJSON Owner where
parseJSON = withObject "Owner" $ \v -> do
ownerData <- parseJSON (Object v)
userType <- v .: "type"
return Owner { owner_data = ownerData, user_type = userType }
我使用的导入和语言编译指示:
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
import Data.Aeson
import Data.Text (Text)
import Data.Monoid ((<>))
import GHC.Generics (Generic)
import qualified Data.HashMap.Lazy as HML (fromList, toList)
JSON 响应 Github Gists Rest API contains Haskell's keyword type
。但是type
不能作为记录字段。
因此它不能用于 Aeson's Generic FromJSON/ToJSON 个实例的实现。
import Data.Text (Text)
import GHC.Generics (Generic)
type URL = Text
data OwnerType = User deriving (Show)
data Owner = Owner {
id :: Int,
gravatar_id :: Text,
login :: Text,
avatar_url :: Text,
events_url :: URL,
followers_url :: URL,
following_url :: URL,
gists_url :: URL,
html_url :: URL,
organizations_url :: URL,
received_events_url :: URL,
repos_url :: URL,
starred_url :: URL,
subscriptions_url :: URL,
url :: URL,
-- type :: Text,
site_admin :: Bool
} deriving (Generic, Show)
instance ToJSON Owner
instance FromJSON Owner
问题: 有没有合适的方法来处理这种冲突?
我们可以使用 TemplateHaskell
来解决这个问题。我们可以使用键的特定映射,而不是写 ToJSON
和 FromJON
。
首先我们要为字段构造一个非类型的名称,例如:
data Owner = Owner {
id :: Int,
gravatar_id :: Text,
login :: Text,
avatar_url :: Text,
events_url :: URL,
followers_url :: URL,
following_url :: URL,
gists_url :: URL,
html_url :: URL,
organizations_url :: URL,
received_events_url :: URL,
repos_url :: URL,
starred_url :: URL,
subscriptions_url :: URL,
url :: URL,
<b>owner_type</b> :: Text,
site_admin :: Bool
} deriving (Generic, Show)
现在我们可以使用 deriveJSON :: Options -> Name -> Q [Dec]
函数构造一个 fromJSON
和 toJSON
实例。
这里的关键是Options
参数:它包含一个fieldLabelModifier :: String -> String
字段,可以将字段的名称重写为[=中的键74=]。因此,我们可以在这里生成一个函数来重写它。
所以我们先构造一个函数ownerFieldRename :: String -> String
:
ownerFieldRename :: String -> String
ownerFieldRename "owner_type" = "type"
ownerFieldRename name = name
所以这个函数作为恒等函数,除了"owner_type"
,映射到"type"
.
现在我们可以使用自定义选项调用 deriveJSON
函数,例如:
$(deriveJSON defaultOptions {fieldLabelModifier = ownerFieldRename} ''Owner)
或完整:
RenameUtils.hs
:
<b>module RenameUtils where
ownerFieldRename :: String -> String
ownerFieldRename "owner_type" = "type"
ownerFieldRename name = name</b>
MainFile.hs
:
<b>{-# LANGUAGE TemplateHaskell #-}</b>
{-# LANGUAGE DeriveGeneric #-}
<b>import Data.Aeson.TH(deriveJSON, defaultOptions, Options(fieldLabelModifier))</b>
<b>import RenameUtils(ownerFieldRename)</b>
import Data.Text (Text)
type URL = Text
data Owner = Owner {
id :: Int,
gravatar_id :: Text,
login :: Text,
avatar_url :: Text,
events_url :: URL,
followers_url :: URL,
following_url :: URL,
gists_url :: URL,
html_url :: URL,
organizations_url :: URL,
received_events_url :: URL,
repos_url :: URL,
starred_url :: URL,
subscriptions_url :: URL,
url :: URL,
<b>owner_type</b> :: Text,
site_admin :: Bool
} deriving (Show)
<b>$(deriveJSON defaultOptions {fieldLabelModifier = ownerFieldRename} ''Owner)</b>
现在我们得到一个 JSON 对象,以 type
为键:
Prelude Main Data.Aeson> encode (Owner 1 "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" True)
"{\"id\":1,\"gravatar_id\":\"\",\"login\":\"\",\"avatar_url\":\"\",\"events_url\":\"\",\"followers_url\":\"\",\"following_url\":\"\",\"gists_url\":\"\",\"html_url\":\"\",\"organizations_url\":\"\",\"received_events_url\":\"\",\"repos_url\":\"\",\"starred_url\":\"\",\"subscriptions_url\":\"\",\"url\":\"\",\"type\":\"\",\"site_admin\":true}"
对于一个简单的fieldLabelModifier
函数我们不需要写一个特定的函数(我们必须在特定的模块中定义),我们也可以使用一个lambda表达式 这里:
MainFile.hs
:
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveGeneric #-}
import Data.Aeson.TH(deriveJSON, defaultOptions, Options(fieldLabelModifier))
import Data.Text (Text)
type URL = Text
data Owner = Owner {
id :: Int,
gravatar_id :: Text,
login :: Text,
avatar_url :: Text,
events_url :: URL,
followers_url :: URL,
following_url :: URL,
gists_url :: URL,
html_url :: URL,
organizations_url :: URL,
received_events_url :: URL,
repos_url :: URL,
starred_url :: URL,
subscriptions_url :: URL,
url :: URL,
<b>owner_type</b> :: Text,
site_admin :: Bool
} deriving (Show)
$(deriveJSON defaultOptions {fieldLabelModifier = <b>\x -> if x == "owner_type" then "type" else x</b>} ''Owner)
Willem 的答案可能更合适,可能更符合你的要求,但这里有另一种方法,允许你定义不冲突的数据,而不必为其编写 ToJSON 和 FromJSON 实例,定义类型
data OwnerData = OwnerData {
oid :: Int
-- ... other data with non-conflicting names
} deriving (Show, Generic)
和
data Owner = Owner {
owner_data :: OwnerData,
user_type :: Text
} deriving (Show)
我们现在可以定义以下实例:
-- nothing special for OwnerData:
instance ToJSON OwnerData
instance FromJSON OwnerData
-- a little helper function to extract the hashmap(Object) from a value
toObject :: ToJSON a => a -> Object
toObject a = case toJSON a of
Object o -> o
_ -> error "toObject: value isn't an Object"
-- the instances for Owner
instance ToJSON Owner where
toJSON (Owner {owner_data = ownerData, user_type = userType}) =
Object $
toObject ownerData <> HML.fromList ["type" .= userType]
toEncoding (Owner {owner_data = ownerData, user_type = userType}) =
pairs . foldMap (uncurry (.=)) . HML.toList $
toObject ownerData <> HML.fromList ["type" .= userType]
instance FromJSON Owner where
parseJSON = withObject "Owner" $ \v -> do
ownerData <- parseJSON (Object v)
userType <- v .: "type"
return Owner { owner_data = ownerData, user_type = userType }
我使用的导入和语言编译指示:
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
import Data.Aeson
import Data.Text (Text)
import Data.Monoid ((<>))
import GHC.Generics (Generic)
import qualified Data.HashMap.Lazy as HML (fromList, toList)