jsonparsinghaskellaeson

How do I parse a JSON in Haskell where the name of the fields can be one of multiple values, but should be converted into a single Haskell type?


Let's say I have the following JSON values.

{
  "fieldName1": 5,
  "value1": "Hello"
}

and

{
  "fieldName2": 7,
  "value1": "Welcome"
}

I have the following type in Haskell.

data Greeting 
  = Greeting
      {
        count :: Int,
        name :: Text
      }
  deriving (Generic, Show, Eq)

How do I parse this JSON into Haskell where fieldName1 or fieldName2 values should be parsed as count value?

I tried to solve this by doing something like shown below.

instance FromJSON Greeting where
  parseJSON = withObject "Greeting" $ \obj -> do
    count1 <- obj .:? "fieldName1"
    count2 <- obj .:? "fieldName2"
    name <- obj .: "value1"
    count <-
      case (count1, count2) of
        (Just count, Nothing) -> return count
        (Nothing, Just count) -> return count
        _ -> fail $ Text.unpack "Field missing"
    return Greeting {count = count, name = name}

It works but is very cumbersome and if there are more than 2 alternative values, it becomes a lot more complex. Is there any way to solve this in a simpler way?


Solution

  • The Parser monad where parseJSON runs is itself an Alternative, so you can use the alternation (<|>) operator within the parser definition:

    instance FromJSON Greeting where
      parseJSON = withObject "Greeting" $ \o -> do
        Greeting <$> (o .: "fieldName1" <|> o .: "fieldName2"
                      <|> fail "no count field")
                 <*> o .: "value1"
    

    If multiple "count" fields are present, this will take the first one that parses.

    If you want to process field names more programmatically (for example, if you want to accept a single field whose name starts with the prefix "field" while rejecting cases with multiple matching fields), then note that o is a KeyMap that can be processed with the functions in the Data.Aeson.KeyMap and Data.Aeson.Key modules:

    import Data.Aeson
    import qualified Data.Aeson.Key as Key
    import qualified Data.Aeson.KeyMap as KeyMap
    import Data.List (isPrefixOf)
    
    instance FromJSON Greeting where
      parseJSON = withObject "Greeting" $ \o -> do
        -- get all values for "field*" keys
        let fields = [v | (k, v) <- KeyMap.toList o
                        , "field" `isPrefixOf` Key.toString k]
        case fields of
          [v] -> Greeting <$> parseJSON v <*> o .: "value1"
          []  -> fail "no \"field*\" fields"
          _   -> fail "multiple \"field*\" fields"