tagged-aeson
provides tagged FromJSON
and ToJSON
classes and TH
generators that use those instances instead of Aeson's ones.
Any function expecting a normal FromJSON
or ToJSON
constraint can work
with tagged-aeson
instances by means of the TaggedAeson
newtype wrapper.
Somewhat, but it's better to wait until an official release. The API is in flux, and feedback is welcome!
See also: monadfix/jijo, a more radical instance-less approach to bidirectional JSON encoding/decoding.
You have a URI
in your config type and you want to autoderive a
FromJSON
instance for the config. Without tagged-aeson
, you'd write an
orphan instance:
{-# OPTIONS_GHC -Wno-orphans #-}
instance FromJSON URI where
parseJSON = withText "URI" $
maybe (fail "invalid URI") return . parseURIReference . unpack
data Config = { ... }
deriveFromJSON defaultOptions ''Config
decodeConfig :: ByteString -> Maybe Config
decodeConfig = decode
With tagged-aeson
, you can write a non-orphan instance – and only use it
for FromJSON Config
without letting it escape into the rest of your
program:
instance FromJSON Config URI where
parseJSON = withText "URI" $
maybe (fail "invalid URI") return . parseURIReference . unpack
data Config = { ... }
deriveFromJSON ''Config defaultOptions ''Config
decodeConfig :: ByteString -> Maybe Config
decodeConfig = fmap (fromTaggedAeson @Config) . decode
You have a request type with a plaintext password in one of the fields. You
are prudent and don't have a ToJSON
instance for your PlainTextPassword
type – after all, you only need to parse requests, not generate them.
That is, until you start writing tests.
Without tagged-aeson
: you would define orphan instances for all your
request types in the testsuite. Or you would write functions
someRequestToJSON
, otherRequestToJSON
, etc, and import them when
necessary.
With tagged-aeson
:
data TestOnly a
instance FromJSON Api PlainTextPassword
instance ToJSON (TestOnly Api) PlainTextPassword
instance FromJSON Api SomeRequest
instance ToJSON (TestOnly Api) SomeRequest
Coming soon!
You are working with a weird API that represents UTCTime
as
/Date(1302547608878)/
. By default, the serialization for UTCTime
is
not what you want, but Aeson provides a newtype to handle this case:
data User = User { name :: Text, created :: UTCTime }
instance FromJSON User where
parseJSON = withObject "User" $ \o ->
name <- o .: "name"
created <- fromDotNetTime <$> o .: "created"
pure User{..}
However, you can forget to use the newtype wrapper, leading to hard-to-find
bugs. With tagged-aeson
, though, you can just change the instance:
data WeirdAPI
instance FromJSON WeirdAPI User where
parseJSON = withObject "User" $ \o ->
name <- o .: "name"
created <- o .: "created"
pure User{..}
instance FromJSON WeirdAPI UTCTime where
parseJSON = fmap fromDotNetTime . using @Aeson parseJSON
You can also write a stub instance with a TypeError
and
direct users towards one of several existing newtypes.
Let's say you really don't like
Aeson instances for Maybe
.
tagged-aeson
provides no built-in instances; you can always lift instances
from Aeson, but you don't have to. Or you can write a stub instance – and,
again, add a TypeError
so that nobody would be able to use
it. At last!