Jonathan
Jonathan

Reputation: 11321

How can I access JSON values from GHCI?

I'm trying to navigate JSON values using Haskell, in GHCI. I can get a JSON payload from an API, with something like this:

import Network.HTTP.Simple

baseURL <- parseRequest "https://www.googleapis.com/books/v1/volumes"
let queryString = B8.pack $ unpack q
let request = setRequestQueryString [("q", Just queryString)] $ baseURL
resp <- httpJSON request
let body = getResponseBody resp :: Object

And that gives me an Object. That object (a HashMap) contains the key "items" whose value is an Array of Objects. I want to get the first object from taht array, then gets its volumeInfo, then its industryIdentifiers, then its isbn.

In Python I would do:

identifiers = body['items'][0]['industryIdentifiers']
isbn = [id['itentifier'] for id in identifiers if id['type'] == 'ISBN_10'][0]

Or in other words, just chain accessors. How can I do this in Haskell? I've tried something like ((body ! "items") !! 0) ! "volumeInfo") but I keep getting errors like Couldn't match expected type ‘[a]’ with actual type ‘Value’.

All the tutorials I can find just say to model the data by creating a complete picture of the data as a Haskell data structure, then writing a decoder to turn that JSON data into a Haskell data object. That seems like massive overkill in this case, when the data structure I'm getting from the API is way bigger than the bit that I need, which is just the ISBN.

How does one normally drill down through a big data structure in Haskell?

Upvotes: 2

Views: 94

Answers (1)

leftaroundabout
leftaroundabout

Reputation: 120741

The preferred way is to not manually deal with JSON values, but instead parse them into a suitable Haskell type and then index into that, which is much safer: if the input doesn't conform to the expected format, you get a clear parsing error show up at which location in the data structure something is missing, instead of an obscure key-missing error somewhere deep in your code.

{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}

data GoogleBooksVolumes = GoogleBooksVolumes
  { items :: Array GoogleBooksVolume
  , ...
  } deriving (Generic, FromJSON, ToJSON)

data GoogleBooksVolume = GoogleBooksVolume
  { ...
  , industryIdentifiers :: Array IndustryIdentifier
  , ...
  } deriving (Generic, FromJSON, ToJSON)

...

If you're going to ad-hoc index into the JSON object, your best bet is the aeson-lens package. That allows you to do something very similar – and similarly unsafe – as in Python.

{-# LANGUAGE OverloadedStrings #-}

Just identifiers = Just body ^. key "items" . nth 0 . key "industryIdentifiers"
Just isbn = head [ Just idf ^. key "identifier"
                 | idf <- identifiers
                 , Just idf ^. key "type" == Just (String "ISBN_10") ]

TBH this is even worse than in Python, because if a key fails to match you just get a Nothing result without any information at all what went wrong.

A safer option is to manually pattern-match at every decision where something could go wrong, but that is a lot of boilerplate.

Upvotes: 1

Related Questions