Search code examples
haskellaeson

How to deal with Haskell's reserved keywords in record fields?


JSON in response of Github Gists Rest API contains Haskell's keyword type. But type couldn't be used as a record field.

Thus it couldn't be used in implementation of Aeson's Generic FromJSON/ToJSON instances.

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

Question: Is there a proper approach to deal with such kind of conflicts?


Solution

  • We can solve this by using TemplateHaskell. Instead of writing ToJSON and FromJON, we can use a specific mapping of the keys.

    First of all, we have to construct a name for the field that is not type, for instance:

    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,
          owner_type :: Text,
          site_admin :: Bool
      } deriving (Generic, Show)

    Now we can use the deriveJSON :: Options -> Name -> Q [Dec] function that will construct a fromJSON and toJSON instance.

    The key here is the Options parameter: it contains a fieldLabelModifier :: String -> String field that can rewrite the names of the fields to the keys in JSON. We can thus here generate a function that will rewrite it.

    So we first construct a function ownerFieldRename :: String -> String:

    ownerFieldRename :: String -> String
    ownerFieldRename "owner_type" = "type"
    ownerFieldRename name = name
    

    So this function acts as an identity function, except for "owner_type", which is mapped on "type".

    So now we can call the deriveJSON function with custom options like:

    $(deriveJSON defaultOptions {fieldLabelModifier = ownerFieldRename} ''Owner)
    

    Or in full:

    RenameUtils.hs:

    module RenameUtils where
    
    ownerFieldRename :: String -> String
    ownerFieldRename "owner_type" = "type"
    ownerFieldRename name = name

    MainFile.hs:

    {-# LANGUAGE TemplateHaskell #-}
    {-# LANGUAGE DeriveGeneric #-}
    
    import Data.Aeson.TH(deriveJSON, defaultOptions, Options(fieldLabelModifier))
    import RenameUtils(ownerFieldRename)
    
    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,
          owner_type :: Text,
          site_admin :: Bool
      } deriving (Show)
    
    $(deriveJSON defaultOptions {fieldLabelModifier = ownerFieldRename} ''Owner)

    Now we obtain a JSON object with type as key:

    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}"
    

    For a simple fieldLabelModifier function we do not need to write a specific function (that we have to define in a specific module), we can also use an lambda expression here:

    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,
          owner_type :: Text,
          site_admin :: Bool
      } deriving (Show)
    
    $(deriveJSON defaultOptions {fieldLabelModifier = \x -> if x == "owner_type" then "type" else x} ''Owner)