Search code examples
jsonhaskellaeson

Can aeson handle JSON with imprecise types?


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.


Solution

  • 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.