Reputation: 97
My question seems to be closely related to this one.
My code parses a yaml file, rearanges the objects and writes a new yaml file. It works perfectly well, but there is a particularly ugly part in it.
I have to declare my data structures as instances of FromJson
and ToJson
like this:
instance FromJSON Users where
parseJSON = genericParseJSON (defaultOptions { fieldLabelModifier = body_noprefix })
instance ToJSON Users where
toJSON = genericToJSON (defaultOptions { fieldLabelModifier = body_noprefix })
The problem is that I have to repeat this for 8 or so other cases:
instance FromJSON Role where
parseJSON = genericParseJSON (defaultOptions { fieldLabelModifier = body_noprefix })
instance ToJSON Role where
toJSON = genericToJSON (defaultOptions { fieldLabelModifier = body_noprefix })
...
...
I could not figure out how to avoid this repetition. Is there some method to declare the two functions just once (for example in a new class) and let all these data types derive from it?
Solution (see also accepted answer by dfeuer):
I personally like this solution. You'll need to add
{-# language DerivingVia #-}
{-# language UndecidableInstances #-}
newtype NP a = NP {unNP::a}
instance (Generic a, GFromJSON Zero (Rep a)) => FromJSON (NP a) where
parseJSON = fmap NP . genericParseJSON
(defaultOptions { fieldLabelModifier = body_noprefix })
instance (Generic a, GToJSON Zero (Rep a)) => ToJSON (NP a) where
toJSON = genericToJSON (defaultOptions { fieldLabelModifier = body_noprefix }) . unNP
Then you can declare the types like this:
data User = User { ... } deriving (Show, Generic)
deriving FromJSON via (NP User)
deriving ToJSON via (NP User)
Upvotes: 5
Views: 297
Reputation: 48611
This is what the fairly new DerivingVia
extension is for, among other things.
{-# language DerivingVia #-}
newtype NP a = NP {unNP::a}
instance (Generic a, GFromJSON Zero (Rep a)) => FromJSON (NP a) where
parseJSON = fmap NP . genericParseJSON
(defaultOptions { fieldLabelModifier = body_noprefix })
instance (Generic a, GToJSON Zero (Rep a)) => ToJSON (NP a) where
toJSON = genericToJSON (defaultOptions { fieldLabelModifier = body_noprefix }) . unNP
Now, you can write
deriving via (NP User) instance FromJSON User
Or
data User = ...
deriving Generic
deriving (FromJSON, ToJSON) via (NP User)
and so on.
This doesn't save a lot over leftaroundabout's answer as it is. However, once you add a definition of toEncoding
, it starts to look worthwhile.
Caution: I have tested none of this.
Upvotes: 5
Reputation: 120731
Like,
noPrefixParseJSON :: (Generic a, GFromJSON Zero (Rep a)) => Value -> Parser a
noPrefixParseJSON
= genericParseJSON (defaultOptions { fieldLabelModifier = body_noprefix })
noPrefixToJSON :: (Generic a, GToJSON Zero (Rep a)) => a -> Value
noPrefixToJSON
= genericToJSON (defaultOptions { fieldLabelModifier = body_noprefix })
instance FromJSON User where parseJSON = noPrefixParseJSON
instance ToJSON User where toJSON = noPrefixToJSON
instance FromJSON Role where parseJSON = noPrefixParseJSON
instance ToJSON Role where toJSON = noPrefixToJSON
...
Sure this is still kind of repetitive, but I'd say this isn't any cause of worry any more.
Upvotes: 2
Reputation: 27766
If explicitly declaring all those similar instances proves too onerous, perhaps you could parameterize your datatypes with a phantom type like
data User x = User { aa :: Int, bb :: Bool } deriving Generic
data Role x = Role { xx :: Int, dd :: Bool } deriving Generic
and then define a "marker" datatype like
data Marker
This datatype will only serve as a peg on which to hang instances like the following
{-# language UndecidableInstances #-}
instance (Generic (f Marker), GFromJSON Zero (Rep (f Marker))) => FromJSON (f Marker) where
parseJSON = noPrefixParseJSON
Would it be worth it? Likely not, because the definition of your datatypes becomes more complex. On the other hand, you could change aspects of the serialization by varying the marker, so you gain some flexibility.
Upvotes: 0