11

I have a JSON doc that looks like:

{ "series": [[1,2], [2,3], [3,4]] }

I'd like to parse this into a set of data types:

data Series = Series [DataPoint]
data DataPoint = DataPoint Int Int  -- x and y

I'm having lots of problems trying to write the FromJSON instance for DataPoint.

instance FromJSON DataPoint where
  parseJSON (Array a) = ???

I've tried using Lens to destruct the DataPoint record, but it doesn't compile:

case a ^.. values . _Integer of -}
  [x,y] -> DataPoint <$> x <*> y
  _     -> mzero

That fails with this error (the first two lines I get even absent the lens trickery, just trying to create a DataPoint <$> 1 <*> 2):

Couldn't match type ‘aeson-0.7.0.6:Data.Aeson.Types.Internal.Parser
                       Integer’
              with ‘Integer’
Expected type: (aeson-0.7.0.6:Data.Aeson.Types.Internal.Parser
                  Integer
                -> Const
                     (Data.Monoid.Endo
                        [aeson-0.7.0.6:Data.Aeson.Types.Internal.Parse
                     (aeson-0.7.0.6:Data.Aeson.Types.Internal.Parser I
               -> Value
               -> Const
                    (Data.Monoid.Endo
                       [aeson-0.7.0.6:Data.Aeson.Types.Internal.Parser
                    Value
  Actual type: (Integer
                -> Const
                     (Data.Monoid.Endo
                        [aeson-0.7.0.6:Data.Aeson.Types.Internal.Parse
                     Integer)
               -> Value
               -> Const
                    (Data.Monoid.Endo
                       [aeson-0.7.0.6:Data.Aeson.Types.Internal.Parser
                    Value
In the second argument of ‘(.)’, namely ‘_Integer’
In the second argument of ‘(^..)’, namely ‘values . _Integer’

Is there a better way to do this?

Does anybody have an example of parsing arrays of values into a more detailed structure?

Chris Stryczynski
  • 30,145
  • 48
  • 175
  • 286
cschneid
  • 460
  • 4
  • 10
  • [Here's an example](https://gist.github.com/bheklilr/98ac8f8e663cf02fcaa6) I wrote up a while ago for someone else, it might give you a good start. – bheklilr Jul 14 '14 at 18:11
  • Thanks bheklilr, but the issue I'm running into isn't Aeson's parsing (Object parsing is easy enough), but focused on destructuring an array into a more semantic data type. The array has `[X, Y]`, where the they are two different semantic meanings, that are only indicated by index. I want to parse that into a real data type `DataPoint Int Int` that I can refine the types and names down to be exactly what it should be meaning. – cschneid Jul 14 '14 at 18:13

2 Answers2

17

Aeson have instance for list, so I think it is not necessary to deal with vectors.

{-# LANGUAGE LambdaCase #-}
import Data.Aeson

data Series = Series [DataPoint]
data DataPoint = DataPoint Int Int

instance FromJSON DataPoint where
  parseJSON jsn = do
    [x,y] <- parseJSON jsn
    return $ DataPoint x y

instance FromJSON Series where
  parseJSON = \case
    Object o -> (o .: "series") >>= fmap Series . parseJSON
    x -> fail $ "unexpected json: " ++ show x
max taldykin
  • 12,459
  • 5
  • 45
  • 64
4

The trick here is getting the instance for FromJSON DataPoint correct, which takes a little bit of matching but isn't too bad. I came up with

instance FromJSON DataPoint where
    parseJSON (Array v)
        | V.length v == 2 = do
            x <- parseJSON $ v V.! 0
            y <- parseJSON $ v V.! 1
            return $ DataPoint x y
        | otherwise = mzero
    parseJSON _ = mzero

Which will fail to parse cleanly if it isn't able to pull two Ints out for x and y. Then you just have to define the instance for Series:

instance FromJSON Series where
    parseJSON (Object o) = do
        pts <- o .: "series"
        ptsList <- mapM parseJSON $ V.toList pts
        return $ Series ptsList
    parseJSON _ = mzero

Which, again, will cleanly fail if the data is malformed anywhere. To test:

> decode "{\"series\": [[1, 2], [3, 4]]}" :: Maybe Series
Just (Series [DataPoint 1 2, DataPoint 3 4])
> decode "{\"series\": [[1, 2], [3, {}]]}" :: Maybe Series
Nothing

So it looks like it works.


EDIT: As @maxtaldykin has pointed out, you can just take advantage of the FromJSON a => FromJSON [a] instance with

instance FromJSON DataPoint where
    parseJSON obj = do
        [x, y] <- parseJSON obj
        return $ DataPoint x y

instance FromJSON Series where
    parseJSON (Object o) = do
        pts <- o .: "series"
        fmap Series $ parseJSON pts
    parseJSON _ = mzero

Which is greatly simplified from my original answer. Kudos to Max.

madjestic
  • 59
  • 8
bheklilr
  • 53,530
  • 6
  • 107
  • 163
  • Thank you for the quick answer earlier. Got me over my hurdle, and I ended up using a combination of both your answer and @max taldykin's. – cschneid Jul 15 '14 at 00:55