Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Parse Array in nested JSON with Aeson

Tags:

haskell

aeson

I'm trying to write a FromJSON function for Aeson.

The JSON:

{
  "total": 1,
  "movies": [
    {
      "id": "771315522",
      "title": "Harry Potter and the Philosophers Stone (Wizard's Collection)",
      "posters": {
        "thumbnail": "http://content7.flixster.com/movie/11/16/66/11166609_mob.jpg",
        "profile": "http://content7.flixster.com/movie/11/16/66/11166609_pro.jpg",
        "detailed": "http://content7.flixster.com/movie/11/16/66/11166609_det.jpg",
        "original": "http://content7.flixster.com/movie/11/16/66/11166609_ori.jpg"
      }
    }
  ]
}

The ADT: data Movie = Movie {id::String, title::String}

My attempt:

instance FromJSON Movie where
    parseJSON (Object o) = do
       movies <- parseJSON =<< (o .: "movies") :: Parser Array
       v <- head $ decode movies
       return $ Movie <$>
           (v .: "movies" >>= (.: "id") ) <*>
           (v .: "movies" >>= (.: "title") )
    parseJSON _ = mzero

This gives Couldn't match expected type 'Parser t0' with actual type 'Maybe a0' In the first argument of 'head'.

As you can see, I'm trying to pick the first of the movies in the Array, but I wouldn't mind getting a list of Movies either (in case there are several in the Array).

like image 917
mb21 Avatar asked May 14 '13 16:05

mb21


2 Answers

If you really want to parse a single Movie from a JSON array of movies, you can do something like this:

instance FromJSON Movie where
    parseJSON (Object o) = do
        movieValue <- head <$> o .: "movies"
        Movie <$> movieValue .: "id" <*> movieValue .: "title"
    parseJSON _ = mzero

But the safer route would be to parse a [Movie] via newtype wrapper:

main = print $ movieList <$> decode "{\"total\":1,\"movies\":[ {\"id\":\"771315522\",\"title\":\"Harry Potter and the Philosophers Stone (Wizard's Collection)\",\"posters\":{\"thumbnail\":\"http://content7.flixster.com/movie/11/16/66/11166609_mob.jpg\",\"profile\":\"http://content7.flixster.com/movie/11/16/66/11166609_pro.jpg\",\"detailed\":\"http://content7.flixster.com/movie/11/16/66/11166609_det.jpg\",\"original\":\"http://content7.flixster.com/movie/11/16/66/11166609_ori.jpg\"}}]}"

newtype MovieList = MovieList {movieList :: [Movie]}

instance FromJSON MovieList where
    parseJSON (Object o) = MovieList <$> o .: "movies"
    parseJSON _ = mzero

data Movie = Movie {id :: String, title :: String}

instance FromJSON Movie where
    parseJSON (Object o) = Movie <$> o .: "id" <*> o .: "title"
    parseJSON _ = mzero
like image 174
Mike Craig Avatar answered Oct 28 '22 23:10

Mike Craig


It's usually easiest to match the structure of your ADTs and instances to the structure of your JSON.

Here, I've added a newtype MovieList to deal with the outermost object so that the instance for Movie only has to deal with a single movie. This also gives you multiple movies for free via the FromJSON instance for lists.

data Movie = Movie { id :: String, title :: String }

newtype MovieList = MovieList [Movie]

instance FromJSON MovieList where
  parseJSON (Object o) =
    MovieList <$> (o .: "movies")
  parseJSON _ = mzero

instance FromJSON Movie where
  parseJSON (Object o) =
    Movie <$> (o .: "id")
          <*> (o .: "title")
  parseJSON _ = mzero
like image 25
hammar Avatar answered Oct 29 '22 01:10

hammar