tkx68
tkx68

Reputation: 101

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.

Upvotes: 3

Views: 148

Answers (2)

danidiaz
danidiaz

Reputation: 27766

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.

Upvotes: 1

Mark Seemann
Mark Seemann

Reputation: 233172

One low-hanging (although perhaps not so elegant) option is to define the property as an Aeson Value. Here's an example:

{-#LANGUAGE DeriveGeneric #-}
module Q65410397 where

import GHC.Generics
import Data.Aeson

data JExample = JExample { jproperty :: Value } deriving (Eq, Show, Generic)

instance ToJSON JExample where

instance FromJSON JExample where

Aeson can decode a JSON value with a number:

*Q65410397> decode "{\"jproperty\":123}" :: Maybe JExample
Just (JExample {jproperty = Number 123.0})

It also works if the value is a string:

*Q65410397> decode "{\"jproperty\":\"123\"}" :: Maybe JExample
Just (JExample {jproperty = String "123"})

Granted, by defining the property as Value this means that at the Haskell side, it could also hold arrays and other objects, so you should at least have a path in your code that handles that. If you're absolutely sure that the third-party service will never give you, say, an array in that place, then the above isn't the most elegant solution.

On the other hand, if it gives you both 123 and "123", there's already some evidence that maybe you shouldn't trust the contract to be well-typed...

Upvotes: 3

Related Questions