json parsing in haskell
Asked Answered
G

3

16

I'm trying to parse JSON data in haskell. Having gone through a slew of websites, this is the furthest I have been able to get to.

data Address = Address { house :: Integer, street :: String, city :: String, state :: String, zip :: Integer } deriving (Show)
data Person = Person { name :: String, age :: Integer, address :: Address } deriving (Show)

getName :: Person -> String
getName (Person n _ _) = n

getAddress :: Person -> Address
getAddress (Person _ _ a) = a

getState :: Address -> String
getState (Address _ _ _ s _) = s

I write that in a file ex.hs and load it in ghci -->

Prelude> import Text.JSON
Prelude Text.JSON> :load ex
Main Text.JSON> let aa = "{\"name\": \"some body\", \"age\" : 23, \"address\" : {\"house\" : 285, \"street\" : \"7th Ave.\", \"city\" : \"New York\", \"state\" : \"New York\", \"zip\" : 10001}}"
...> decode aa :: Result JSValue

It returns

Ok (JSObject (JSONObject {fromJSObject = [("name",JSString (JSONString {fromJSString = "some body"})),("age",JSRational False (23 % 1)),("address",JSObject (JSONObject {fromJSObject = [("house",JSRational False (285 % 1)),("street",JSString (JSONString {fromJSString = "7th Ave."})),("city",JSString (JSONString {fromJSString = "New York"})),("state",JSString (JSONString {fromJSString = "New York"})),("zip",JSRational False (10001 % 1))]}))]}))

Needless to say, it seems pretty verbose (and frightening). I tried doing

...> decode aa :: Result Person

and it gave me an error. How do I go about populating an instance of the Person datastructure from this json string? For example, what should I do to get the state of the person in the JSON string...

Geerts answered 24/7, 2013 at 20:29 Comment(0)
M
28

The problem is that Text.JSON does not know how to convert JSON data to your Person data type. To do this, you need to either make Person and instance of the JSON typeclass, or your can use Text.JSON.Generic and the DeriveDataTypeable extension to do the work for you.

Generics

The Text.JSON.Generic method will read the JSON structure based on the structure of your data type.

{-# LANGUAGE DeriveDataTypeable #-}
import           Text.JSON.Generic

data Address = Address
    { house  :: Integer
    , street :: String
    , city   :: String
    , state  :: String
    , zip    :: Integer
    } deriving (Show, Data, Typeable)

data Person = Person
    { name    :: String
    , age     :: Integer
    , address :: Address
    } deriving (Show, Data, Typeable)

aa :: String
aa = "{\"name\": \"some body\", \"age\" : 23, \"address\" : {\"house\" : 285, \"street\" : \"7th Ave.\", \"city\" : \"New York\", \"state\" : \"New York\", \"zip\" : 10001}}"

main = print (decodeJSON aa :: Person)

This method works really well as long as you don't mind matching the names of the fields in your data structure to your JSON format.

As an aside, you don't need to write functions like getName, getAddress, and getState. The names of the field in your record type are accesor functions.

∀ x. x ⊦ :t house
house :: Address -> Integer
∀ x. x ⊦ :t address
address :: Person -> Address

JSON Instance

Alternatively, you could take the high road and implement your own instance of the JSON class.

import           Control.Applicative
import           Control.Monad
import           Text.JSON

data Address = Address
    { house  :: Integer
    , street :: String
    , city   :: String
    , state  :: String
    -- Renamed so as not to conflict with zip from Prelude
    , zipC   :: Integer
    } deriving (Show)

data Person = Person
    { name    :: String
    , age     :: Integer
    , address :: Address
    } deriving (Show)

aa :: String
aa = "{\"name\": \"some body\", \"age\" : 23, \"address\" : {\"house\" : 285, \"street\" : \"7th Ave.\", \"city\" : \"New York\", \"state\" : \"New York\", \"zip\" : 10001}}"

-- For convenience
(!) :: (JSON a) => JSObject JSValue -> String -> Result a
(!) = flip valFromObj

instance JSON Address where
    -- Keep the compiler quiet
    showJSON = undefined

    readJSON (JSObject obj) =
        Address        <$>
        obj ! "house"  <*>
        obj ! "street" <*>
        obj ! "city"   <*>
        obj ! "state"  <*>
        obj ! "zip"
    readJSON _ = mzero

instance JSON Person where
    -- Keep the compiler quiet
    showJSON = undefined

    readJSON (JSObject obj) =
        Person       <$>
        obj ! "name" <*>
        obj ! "age"  <*>
        obj ! "address"
    readJSON _ = mzero

main = print (decode aa :: Result Person)

This takes advantage of the fact that the Result type is an Applicative to easily chain together queries on the JSObject value.

This is a little more work, but it gives you more control of the structure of the JSON if you have to deal with JSON that will cause style guideline violations due to weird field names.

Macleod answered 24/7, 2013 at 20:49 Comment(4)
Maybe you should also give an example of creating an instance of JSON since you mentioned it as an alternative.Nancee
Very useful information. I have a question. Apart from Text.JSON.Generic (which package does it come from?), I've also found hackage.haskell.org/package/generic-aeson similarly employing the Generics machinery to make JSON instances of Haskell data. What are the differences between these two packages?Interact
bergmark: "note that Text.JSON is from the json package which is old and not commonly used anymore."Interact
The json package: hackage.haskell.org/package/json or you can use in the terminal: cabal install jsonBeckmann
W
14

Maybe a bit late in the game, but since this is the first page google returns I'll give it a go.

Aeson is the defacto standard these days so that's the library everybody uses. The Aeson TH package offers some nice functionality for automatically generating the necessary functions for your custom data types.

Basically you create your data types that correspond to the json data and then let aeson do the magic.

{-# LANGUAGE OverloadedStrings,TemplateHaskell #-}
import Data.Aeson
import Data.Aeson.TH
import qualified Data.ByteString.Lazy.Char8 as BL

data Address = Address
    { house  :: Integer
    , street :: String
    , city   :: String
    , state  :: Maybe String
    , zip    :: Integer
    } deriving (Show, Eq)

data Person = Person
    { name    :: String
    , age     :: Integer
    , address :: Address
    } deriving (Show, Eq)

$(deriveJSON defaultOptions ''Address)
$(deriveJSON defaultOptions ''Person)

aa :: BL.ByteString
aa = "{\"name\": \"some body\", \"age\" : 23, \"address\" : {\"house\" : 285, \"street\" : \"7th Ave.\", \"city\" : \"New York\", \"state\" : \"New York\", \"zip\" : 10001}}"

main = print (decode aa :: Maybe Person)

You can even have optional fields with the Maybe datatype.

Wilbert answered 8/12, 2015 at 19:22 Comment(1)
Aeson has to link a lot more libraries than the json packageBeckmann
E
1

Since the answers are a bit old and the Haskell ecosystem has improved a little since, let me mention that aeson with generic-aeson seems to be the best option so far:

{-# language DeriveGeneric #-}

import GHC.Generics (Generic)
import Generics.Generic.Aeson

data Terms = Terms
  { termsEnd :: Day
  , termsStart :: Day
  , termsType :: Text
  , termsParty :: Text
  , termsState :: Text
  }
  deriving (Generic, Show, Eq)

stripSettings :: Settings
stripSettings = defaultSettings {stripPrefix = Just "terms"}

instance FromJSON Terms where parseJSON = gparseJsonWithSettings stripSettings

I started off using Chris Penner's JSON to Haskell tool (which is awesome on its own btw) and then added the Generics instances to make it work without boilerplate.

I think this is the best solution so far because it allows you to decode JSON fields even when they use Haskell keywords (like type, in my example) via gparseJsonWithSettings and automatic parsing of dates to Data.Time.Calendar.Day just works seamlessly!! 💯

Episcopalian answered 17/12, 2021 at 11:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.