问题
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 Char
s, 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 map
ping, 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