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?

2
  • Here's an example I wrote up a while ago for someone else, it might give you a good start. Commented Jul 14, 2014 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. Commented Jul 14, 2014 at 18:13

2 Answers 2

17

Aeson has an 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
Sign up to request clarification or add additional context in comments.

2 Comments

I have to admit that I like this answer better, I didn't even think that lists might have an instance already.
I was also surprised to see it.
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.

1 Comment

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.