1

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’
Mittenchops
  • 18,633
  • 33
  • 128
  • 246

2 Answers2

3

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
Fyodor Soikin
  • 78,590
  • 9
  • 125
  • 172
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
Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555
  • Thank you. This gives me `:8:35: error: Ambiguous occurrence ‘toLower’` What is the customary alias given to the import of Data.Char? I have Prelude, Data.Text, Data.ByteString as BS, Data.Text.Lazy.IO as I. Half a dozen other string conversions. I am still learning, but I have yet to find a problem that hasn't required me to import and juggle every string library possible, unfortunately. – Mittenchops Apr 23 '19 at 20:08
  • @Mittenchops: you can just hide the `toLower` of your `Text`, for example with `import Data.Text hiding (toLower)`. – Willem Van Onsem Apr 23 '19 at 20:10
  • But that would prevent me from using it, right? I am using it in other places of my program. – Mittenchops Apr 23 '19 at 20:12
  • 1
    @Mittenchops: well you can define two import statements: `import Data.Text hiding (toLower)` and `import qualified Data.Text as T` for example. Or just import the `Data.Char` in a qualified way. – Willem Van Onsem Apr 23 '19 at 20:13
  • Thank you. This is very helpful. I'm confused by this `Note: As for the smart constructor, you can simplify this expression to: makeR3 = R3` My understanding was that making functions that create data types was preferable to just using the naked data type itself. Is that not the case? – Mittenchops Apr 25 '19 at 05:57
  • 1
    @Mittenchops: a smart constructor is typically used to (a) prevent exporting the data constructor, and thus preventing a user of the module to *unpack* a constructor, which here is still done, and (b) to do pre-processing an validation, but here you did not write any pre-processing or validation logic. Hence we can define the `makeR3` function as `makeR3 = R3`. But we can not use this function to *unpack* data. – Willem Van Onsem Apr 25 '19 at 07:08
  • Thank you! This is helpful! – Mittenchops Apr 25 '19 at 16:03