I have to deal with JSON from a service that sometimes gives me "123"
instead of 123
as the value of field. Of course this is ugly, but I cannot change the service. Is there an easy way to derive an instance of FromJSON
that can handle this? The standard instances derived by means of deriveJSON
(https://hackage.haskell.org/package/aeson-1.5.4.1/docs/Data-Aeson-TH.html) cannot do that.
Assuming you want to avoid writing FromJSON
instances by hand as much as possible, perhaps you could define a newtype over Int
with a hand-crafted FromJSON
instance—just for handling that oddly parsed field:
{-# LANGUAGE TypeApplications #-}
import Control.Applicative
import Data.Aeson
import Data.Text
import Data.Text.Read (decimal)
newtype SpecialInt = SpecialInt { getSpecialInt :: Int } deriving (Show, Eq, Ord)
instance FromJSON SpecialInt where
parseJSON v =
let fromInt = parseJSON @Int v
fromStr = do
str <- parseJSON @Text v
case decimal str of
Right (i, _) -> pure i
Left errmsg -> fail errmsg
in SpecialInt <$> (fromInt <|> fromStr)
You could then derive FromJSON
for records which have a SpecialInt
as a field.
Making the field a SpecialInt
instead of an Int
only for the sake of the FromJSON
instance feels a bit intrusive though. "Needs to be parsed in an odd way" is a property of the external format, not of the domain.
In order to avoid this awkwardness and keep our domain types clean, we need a way to tell GHC: "hey, when deriving the FromJSON
instance for my domain type, please treat this field as if it were a SpecialInt
, but return an Int
at the end". That is, we want to deal with SpecialInt
only when deserializing. This can be done using the "generic-data-surgery" library.
Consider this type
{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics
data User = User { name :: String, age :: Int } deriving (Show,Generic)
and imagine we want to parse "age" as if it were a SpecialInt
. We can do it like this:
{-# LANGUAGE DataKinds #-}
import Generic.Data.Surgery (toOR', modifyRField, fromOR, Data)
instance FromJSON User where
parseJSON v = do
r <- genericParseJSON defaultOptions v
-- r is a synthetic Data which we must tweak in the OR and convert to User
let surgery = fromOR . modifyRField @"age" @1 getSpecialInt . toOR'
pure (surgery r)
Putting it to work:
{-# LANGUAGE OverloadedStrings #-}
main :: IO ()
main = do
print $ eitherDecode' @User $ "{ \"name\" : \"John\", \"age\" : \"123\" }"
print $ eitherDecode' @User $ "{ \"name\" : \"John\", \"age\" : 123 }"
One limitation is that "generic-data-surgery" works by tweaking Generic
representations, so this technique won't work with deserializers generated using Template Haskell.