你如何用Aeson解析Bitly响应JSON?

时间:2014-03-19 18:00:42

标签: json haskell aeson

我一直在试图用Aeson来解析Bitly的反应。 有人可以给我一个关于应该定义什么Haskell类型的提示 以及如何使用Aeson将以下内容解析为这些类型?:

// BITLY EXPAND RESPONSE
{
  "data": {
    "expand": [
      {
        "global_hash": "900913",
        "long_url": "http://google.com/",
        "short_url": "http://bit.ly/ze6poY",
        "user_hash": "ze6poY"
      }
    ]
  },
  "status_code": 200,
  "status_txt": "OK"
}

// BITLY SHORTEN RESPONSE
{
  "data": {
    "global_hash": "900913",
    "hash": "ze6poY",
    "long_url": "http://google.com/",
    "new_hash": 0,
    "url": "http://bit.ly/ze6poY"
  },
  "status_code": 200,
  "status_txt": "OK"
}

这是我到目前为止所尝试的内容:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}

module BitlyClientResponses where

import           Control.Applicative
import           Data.Aeson
import qualified Data.ByteString.Lazy.Char8 as L (pack)
import qualified Data.HashMap.Strict        as M

data DataStatusCodeStatusTxt =
    DSCST { ddata       :: ResponseData
          , status_code :: Integer
          , status_txt  :: String
          }
    deriving (Eq, Show)

data ResponseData
  = ExpandResponseData { expand :: [Response]
                       }
    deriving (Eq, Show)

data Response = ExpandResponse { long_url    :: String -- URI
                               , global_hash :: String
                               , short_url   :: String -- URI
                               , user_hash   :: String
                               -- , hash        :: [String]
                               -- , error       :: String
                               }
              | J String
              | N String
    deriving (Eq, Show)

instance FromJSON DataStatusCodeStatusTxt where
    parseJSON (Object o) = DSCST <$>
                               o .: "data" <*>
                               o .: "status_code" <*>
                               o .: "status_txt"
    parseJSON x = fail $ "FAIL: DataStatusCodeStatusTxt: " ++ (show x)

instance FromJSON ResponseData where
    parseJSON (Object o) =
        case M.lookup "expand" o of
            -- LOST RIGHT HERE
            Just v  -> return $ ExpandResponseData [J ((show o) ++ " $$$ " ++ (show v))]
            Nothing -> return $ ExpandResponseData [N "N"]
    parseJSON x =  fail $ "FAIL: ResponseData: " ++ (show x)

instance FromJSON Response where
    parseJSON (Object o) = ExpandResponse         <$>
                               o .: "long_url"    <*>
                               o .: "global_hash" <*>
                               o .: "short_url"   <*>
                               o .: "user_hash"
                               -- o .: "hash"        <*>
                               -- o .: "error"       <*>
    parseJSON x =  fail $ "FAIL: Response: " ++ (show x)

parseResponse :: String -> Either String DataStatusCodeStatusTxt
parseResponse x = eitherDecode $ L.pack x

当我输入(为便于阅读而编辑的手):

"{ \"status_code\": 200,
   \"status_txt\": \"OK\",
   \"data\": { \"expand\": [
                            { \"short_url\": \"http:\\/\\/bit.ly\\/LCJq0b\",
                              \"long_url\": \"http:\\/\\/blog.swisstech.net\\/2012\\/06\\/local-postfix-as-relay-to-amazon-ses.html\",
                              \"user_hash\": \"LCJq0b\",
                              \"global_hash\": \"LCJsVy\" }, ...

我回来了(手工编辑):

Right
  (Right
    (DSCST
      {ddata = ExpandResponseData {expand = [J "fromList [(\"expand\",Array (fromList [Object fromList [(\"long_url\",String \"http://blog.swisstech.net/2012/06/local-postfix-as-relay-to-amazon-ses.html\"),(\"global_hash\",String \"LCJsVy\"),(\"short_url\",String \"http://bit.ly/LCJq0b\"),(\"user_hash\",String \"LCJq0b\")], ...
$$$
Array (fromList [Object fromList [(\"long_url\",String \"http://blog.swisstech.net/2012/06/local-postfix-as-relay-to-amazon-ses.html\"),(\"global_hash\",String \"LCJsVy\"),(\"short_url\",String \"http://bit.ly/LCJq0b\"),(\"user_hash\",String \"LCJq0b\")], ...

在代码中,查找-- LOST RIGHT HERE。我无法弄清楚如何解析"expand"的数组。

很高兴看到如何取得进步。也许我走错了路,有人可以直截了当(例如,我到目前为止定义的数据类型可能已关闭)。

1 个答案:

答案 0 :(得分:4)

有效使用Aeson的诀窍是递归调用parseJSON。当您使用(.:)运算符时,这是隐式执行的,因此查看类似M.lookup的内容通常是一个不好的信号。我将提供一个简化示例:(纬度,经度)对的路径,由JSON对象的JSON数组表示。

data Path  = Path  { points :: [Point] }
data Point = Point { lat :: Double, lon :: Double }

-- JSON format looks a bit like this
--
-- { "points": [ {"latitude": 86, "longitude": 23} ,
--               {"latitude": 0,  "longitude": 16} ,
--               {"latitude": 43, "longitude": 87} ] }

instance FromJSON Path where
  parseJSON = withObject "path" $ \o -> 
    Path <$> o .: "points"

instance FromJSON Point where
  parseJSON = withObject "point" $ \o ->
    Point <$> o .: "latitude"
          <*> o .: "longitude"

要从这个片段中删除两个要点。首先,请注意使用withObject快速约束传递给Value的{​​{1}}被标记为parseJSON - 它与使用模式匹配没有显着差异,但它会产生自动,统一的错误消息,所以值得考虑。

其次,更重要的是,请注意我只定义描述每个对象的高级轮廓的Object实例。特别是,检查FromJSON

的正文
FromJSON Path

所有这些都说明我需要查看名为Path <$> o .: "points" 的条目,并尝试将其解析为构建"points"所需的任何类型 - 在这种情况下,{{1}列表}},Path。此用法取决于递归定义的Point实例。我们需要解析一个数组,但幸运的是已经存在[Point]实例

FromJSON

被解释为JSON类型FromJSON可以解析为的JSON数组。在我们的案例instance FromJSON a => FromJSON [a] where ... 中,我们只定义该实例

a

然后递归依赖

a ~ Point

这是非常标准的。


您可以使用的另一个重要技巧是使用instance FromJSON Point where ... 连接多个解析。我将简化instance FromJSON Double where ... 数据类型,它将其解析为特定的(<|>)或失败,并生成一个普通的动态类型Response作为默认值。首先,我们将独立编写每个解析器。

Object

现在我们将它们组合在实际的Value实例

data Obj = Obj { foo :: String, bar :: String }
         | Dyn Value

okParse :: Value -> Parser Obj
okParse = withObject "obj" (\o -> Obj <$> o .: "foo" <*> o .: "bar")

elseParse :: Value -> Parser Obj
elseParse v = pure (Dyn v)

在这种情况下,FromJSON会首先尝试使用instance FromJSON Obj where parseJSON v = okParse v <|> elseParse v ,如果失败,则会回到aeson。由于okParse只是一个elseParse值,因此它永远不会失败,从而提供“默认”回退。