9

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).

mb21
  • 34,845
  • 8
  • 116
  • 142

2 Answers2

11

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
Mike Craig
  • 1,677
  • 1
  • 13
  • 22
8

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
hammar
  • 138,522
  • 17
  • 304
  • 385
  • thanks! I didn't think of introducing another type. May I ask a quick follow up? If I'd like to extend the `Movie` type to include some more fields like `filePath` or `myRating`, would you recommend adding a new type `myMovie`, or introducing a few `Maybe` fields to the `Movie` type and fill them up after the `decode`? (I guess filling up would actually mean making a new instance with all fields, since ADT are immutable..) – mb21 May 15 '13 at 09:05
  • 1
    @mb21: Either approach will work fine. It depends on the rest of your application. If those fields are always added immediately after decoding, it might make sense to make a new type so that the rest of your functions don't have to deal with a `Maybe` that should always be `Just`. On the other hand, if those fields are optional it makes sense to keep them in a `Maybe`. – hammar May 15 '13 at 10:59