3
votes

I'm trying to parse JSON to produce a type with multiple constructors. The challenge is that the type is encoded in the name of a key which contains the required data. In theory I could use a bunch of .:? calls and then check if given key returns Just but I think there must be a better way. I looked at asum but this didn't help me much (probably because of my unfamiliarity with it).

import Data.Aeson
import Data.Time.Clock

data Request = Req1 { id :: String, properties :: Value }
             | Req2 { id :: String, properties :: Value }
             | Req3 { id :: String, time :: UTCTime }

instance FromJSON Request where
  parseJSON = withObject "message" $ \o ->
    -- ???

Example requests:

{"req1": {"id": "345", "p1": "v1", "p2": "v2"}}

{"req2": {"id": "654", "p3", "v3"}}

{"req3": {"id": "876", "time": 1234567890}}
1
Can you not use a different type for each distinct request object?jkeuhlen

1 Answers

4
votes

Here's how to manually inspect an Object:

{-# LANGUAGE OverloadedStrings #-}

import Data.Aeson
import Data.Time.Clock
import qualified Data.HashMap.Strict as H
import Control.Monad

type Val = Int

data Request = Req1 { id :: String, properties :: Val }
             | Req2 { id :: String, properties :: Val }
             | Req3 { id :: String, time :: UTCTime }

instance FromJSON Request where
  parseJSON (Object v) =
    case H.lookup "req1" v of
      Just (Object h) -> Req1 <$> h .: "id" <*> h .: "properties"
      Nothing -> 
        case H.lookup "req2" v of
          Just (Object h) -> Req2 <$> h .: "id" <*> h .: "properies"
          Nothing ->
            case H.lookup "req3" v of
              Just (Object h) -> Req3 <$> h .: "id" <*> h .: "time"
              Nothing -> mzero

If the key req1 exists it will assume it is a Req1 value; else if the key req2 exists it will try to parse it as a Req2 value; etc. for req3. If none of those keys exist it will fail.

Instead of mzero you can also use fail "..." to display a custom error message.