I have a Map where the key is a newtype of Text. I would like to automatically (as much as possible) derive ToJSON and FromJSON for this Map. aeson already has instances for ToJSON and FromJSON for Map Text v.
My verbose code that works:
{-# LANGUAGE DeriveGeneric #-}
module Test where
import ClassyPrelude
import Data.Aeson
import GHC.Generics (Generic)
import qualified Data.Map as M
newtype MyText = MyText {unMyText::Text} deriving (Eq, Ord)
data Bar = Bar deriving (Generic)
instance ToJSON Bar
instance FromJSON Bar
data Foo = Foo (Map MyText Bar)
instance ToJSON Foo where
toJSON (Foo x) = toJSON mp
where mp = M.fromList . map (\(x,y) -> (unMyText x,y)) . M.toList $ x
instance FromJSON Foo where
parseJSON v = convert <$> parseJSON v
where convert :: Map Text Bar -> Foo
convert = Foo . mapFromList . map (\(x,y) -> (MyText x,y)) . mapToList
Can I do something more like the following?
data Foo = Foo (Map MyText Bar) deriving (Generic)
instance ToJSON Foo
instance FromJSON Foo
I tried (but still no luck):
newtype MyText = MyText {unMyText::Text} deriving (Eq, Ord, ToJSON, FromJSON)
instance ToJSON Foo where
toJSON (Foo x) = toJSON x
and
newtype MyText = MyText {unMyText::Text} deriving (Eq, Ord, ToJSON, FromJSON)
instance ToJSON Foo
The fact that you can't automatically derive this instance is 100% correct behavior. The reason that what you expect doesn't work is that there is no way to know that the instance FromJSON (Map Text v)
can be used on values of type Map MyText v
. This is because the creation and manipulation of a Map
is predicated on the Ord
instance of its key, and there is no way (for the compiler) to know that for all x y (x == y) == (MyText x == MyText y)
, which is required to safely coerce Map Text v
to Map MyText v
. More technically, the role declaration of Map
is:
type role Map nominal representational
Essentially this says that Map k v
is only coercible to other maps whose first type parameter is identical. The wiki says:
we have instance Coercible a b => Coercible (T a) (T b) if and only if the first parameter has a representational role.
The class Coercible
is used to do type safe coercions in recent versions of GHC (7.8?)
For more information about type roles and their role in type safety, see here. The
If you plan to derive the instance for Ord MyText
, then it is indeed safe to coerce Map Text v
to Map MyText v
, since the Ord
instance is the same. This requires the use of unsafeCoerce
. You still have to write the instance yourself, though:
instance ToJSON v => ToJSON (Map MyText v) where
toJSON = toJSON . (unsafeCoerce :: Map MyText v -> Map Text v)
instance FromJSON v => FromJSON (Map MyText v) where
parseJSON = (unsafeCoerce :: Parser (Map Text v) -> Parser (Map MyText v)) . parseJSON
If you plan to write your own Ord
instance, the above is absolutely not safe. Your solution is correct, but not very efficient. Use the following:
toJSON = toJSON . M.mapKeys (coerce :: MyText -> Text)
parseJSON = fmap (M.mapKeys (coerce :: Text -> MyText)) . parseJSON
Depending on your Ord instance, you may be able to use mapKeysMonotonic
instead, which would be more efficient. See the documentation of Data.Map
for precisely when you can use mapKeysMonotonic
.
Then, the obvious things will work:
data Bar = Bar deriving (Eq, Ord, Generic)
instance ToJSON Bar
instance FromJSON Bar
data Foo = Foo (Map MyText Bar) deriving (Generic)
instance ToJSON Foo
instance FromJSON Foo
-- Using GeneralizedNewtypeDeriving
newtype Foo2 = Foo2 (Map MyText Bar) deriving (FromJSON, ToJSON)
Full code:
{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving, FlexibleInstances #-}
module Test where
import Data.Aeson
import GHC.Generics (Generic)
import qualified Data.Map as M
import Data.Map (Map)
import Data.Text (Text)
import GHC.Prim (coerce)
import Unsafe.Coerce (unsafeCoerce)
import Data.Aeson.Types (Parser)
newtype MyText = MyText {unMyText::Text} deriving (Eq, Ord, Generic, ToJSON, FromJSON)
instance ToJSON v => ToJSON (Map MyText v) where
-- toJSON = toJSON . M.mapKeys (coerce :: MyText -> Text)
toJSON = toJSON . (unsafeCoerce :: Map MyText v -> Map Text v)
instance FromJSON v => FromJSON (Map MyText v) where
-- parseJSON x = fmap (M.mapKeys (coerce :: Text -> MyText)) (parseJSON x)
parseJSON x = (unsafeCoerce :: Parser (Map Text v) -> Parser (Map MyText v)) (parseJSON x)
data Bar = Bar deriving (Eq, Ord, Generic)
instance ToJSON Bar
instance FromJSON Bar
data Foo = Foo (Map MyText Bar) deriving (Generic)
instance ToJSON Foo
instance FromJSON Foo
newtype Foo2 = Foo2 (Map MyText Bar) deriving (FromJSON, ToJSON)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With