Functionally changing key names in serialization to aeson with Text keys

▼魔方 西西 提交于 2019-12-12 19:30:22

问题


I have a json object with a manually crafted ToJSON instance. I would like to replace this with a function that does not require my explicit enumeration of the key names.

I am using "rec*" as a prefix I would like to strip, and my fields start out as Text rather than string.

Starting with minimal data:

data R3 = R3 { recCode :: Code 
             , recValue :: Value} deriving (Show, Generic)

And smart constructor function:

makeR3 rawcode rawval = R3 code value where
                                     code = rawcode
                                     value = rawval

This implementation works fine:

instance ToJSON R3 where
   toJSON (R3 recCode recValue) = object [ "code" .= recCode, "value" .= recValue]

But as you can imagine, typing out every key name by hand from "code" to "recCode" is not something I want to do.

tmp_r3 = makeR3 "TD" "100.42"
as_json = encode tmp_r3

main = do
    let out = encodeToLazyText tmp_r3
    I.putStrLn out
    I.writeFile "./so.json" out
    return ()

Output is correct:

{"value":100.42,"code":"TD"}
-- not recValue and recCode, correct!

However, when I try this function, it becomes unable to convert the text to string as it had automatically before.

instance ToJSON R3 where
  toJSON = genericToJSON defaultOptions {
             fieldLabelModifier = T.toLower . IHaskellPrelude.drop 3 }

Output:

<interactive>:8:35: error:
    • Couldn't match type ‘Text’ with ‘String’
      Expected type: String -> String
        Actual type: String -> Text
    • In the ‘fieldLabelModifier’ field of a record
      In the first argument of ‘genericToJSON’, namely ‘defaultOptions {fieldLabelModifier = toLower . IHaskellPrelude.drop 3}’
      In the expression: genericToJSON defaultOptions {fieldLabelModifier = toLower . IHaskellPrelude.drop 3}
<interactive>:8:47: error:
    • Couldn't match type ‘String’ with ‘Text’
      Expected type: String -> Text
        Actual type: String -> String
    • In the second argument of ‘(.)’, namely ‘IHaskellPrelude.drop 3’
      In the ‘fieldLabelModifier’ field of a record
      In the first argument of ‘genericToJSON’, namely ‘defaultOptions {fieldLabelModifier = toLower . IHaskellPrelude.drop 3}’

The error itself is clear enough that Text doesn't work, but what should I change to strip my prefixes from keynames functionally in json output and also correctly convert text to string?

I am also a little confused that I didn't change my input, it was Text type in both instances, but the first implementation was OK to work with it, while the second was not.

I am working in an ihaskell jupyter notebook.

Update

When I use the Data.Char recommended in answers below:

import Data.Char(toLower)

In:

instance ToJSON R3 where
  toJSON = genericToJSON defaultOptions {
             fieldLabelModifier = Data.Char.toLower . IHaskellPrelude.drop 3 }

I get:

<interactive>:8:35: error:
    • Couldn't match type ‘Char’ with ‘String’
      Expected type: String -> String
        Actual type: String -> Char
    • In the ‘fieldLabelModifier’ field of a record
      In the first argument of ‘genericToJSON’, namely ‘defaultOptions {fieldLabelModifier = Data.Char.toLower . IHaskellPrelude.drop 3}’
      In the expression: genericToJSON defaultOptions {fieldLabelModifier = Data.Char.toLower . IHaskellPrelude.drop 3}
<interactive>:8:55: error:
    • Couldn't match type ‘String’ with ‘Char’
      Expected type: String -> Char
        Actual type: String -> String
    • In the second argument of ‘(.)’, namely ‘IHaskellPrelude.drop 3’
      In the ‘fieldLabelModifier’ field of a record
      In the first argument of ‘genericToJSON’, namely ‘defaultOptions {fieldLabelModifier = Data.Char.toLower . IHaskellPrelude.drop 3}’

And when I try a naked "drop" rather than an IHaskellPrelude drop, I get:

instance ToJSON R3 where
  toJSON = genericToJSON defaultOptions {
             fieldLabelModifier = Data.Char.toLower . drop 3 }

<interactive>:8:55: error:
    Ambiguous occurrence ‘drop’
    It could refer to either ‘BS.drop’, imported from ‘Data.ByteString’
                          or ‘IHaskellPrelude.drop’, imported from ‘Prelude’ (and originally defined in ‘GHC.List’)
                          or ‘T.drop’, imported from ‘Data.Text’

回答1:


You compose two function T.toLower and drop 3, but the types do not match. Indeed, if we lookup the types, we see toLower :: Text -> Text and drop :: Int -> [a] -> [a]. A String is a list of Chars, but Text is not: a Text can be seen as a packed "block" of characters.

We can however compose a function of type String -> String, the type of the field fieldLabelModifier :: String -> String:

import Data.Char(toLower)

instance ToJSON R3 where
    toJSON = genericToJSON defaultOptions {
            fieldLabelModifier = map toLower . drop 3
        }

We thus use the toLower :: Char -> Char function of the Data.Char module, and perform a mapping, such that all characters in the string are mapped.

Note that if you simply want to derive FromJson and ToJSON with different options, you can make use of template Haskell, like:

{-# LANGUAGE DeriveGeneric, TemplateHaskell #-}

import Data.Char(toUpper)
import Data.Aeson.TH(deriveJSON, defaultOptions, Options(fieldLabelModifier))

data Test = Test { attribute :: String } deriving Show

$(deriveJSON defaultOptions {fieldLabelModifier = map toUpper . drop 3} ''Test)

In that case the template Haskell part will implement the FromJSON and ToJSON instances.

Note: We can use qualified imports in order to make it more clear what function we use, for example:
import qualified Data.List as L
import qualified Data.Char as C

instance ToJSON R3 where
    toJSON = genericToJSON defaultOptions {
            fieldLabelModifier = map C.toLower . L.drop 3
        }

Note: As for the smart constructor, you can simplify this expression to:

makeR3 = R3



回答2:


You seem to be using toLower from Data.Text, which works with Text, not with String, so quite naturally, it doesn't fit there.

Instead, you could use toLower from Data.Char and map it over the String:

fieldLabelModifier = map toLower . drop 3


来源:https://stackoverflow.com/questions/55818022/functionally-changing-key-names-in-serialization-to-aeson-with-text-keys

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!