3
votes

Every example I've seen for ToJSON and FromJSON are for data types with single constructors, like so :

data RewindConfig = RConfig JobID Phase
                      deriving Show

instance FromJSON RewindConfig where
  parseJSON (Object o) = RConfig
    <$> o .: "JobID"
    <*> o .: "Phase"
  parseJSON _ = fail "invalid RewindConfig"

I thought I would look to how Aeson makes the instance for a type with multiple constructors, for example Either:

instance (FromJSON a, FromJSON b) => FromJSON (Either a b) where
   parseJSON (Object (H.toList -> [(key, value)]))
        | key == left  = Left  <$> parseJSON value
        | key == right = Right <$> parseJSON value
   parseJSON _        = fail ""

The pattern-matching in parseJSON confuses me, I don't understand what is going on with (H.toList -> [(key, value)]).

The data type I want to make instances for looks like this:

data Foo = Bar String
         | Baz String
         | Bin String

it did occur to me to do something I knew how to implement

data Foo = (Maybe Bar) (Maybe Baz) (Maybe Bin)

But that seems unsatisfying. Could someone help me out by explaining what's going on with the Either instance, and perhaps giving me some guidance on To/From instances for Foo?

update: I think the instances Aeson implements for Maybe are much clearer and tells me what I need to know for my needs. Still, I'd like to know what's going on with Either.

3

3 Answers

4
votes

The pattern (Object (H.toList -> [(key, value)])) is called a view pattern. You can read it as something like this:

parseJSon (Object o) = case H.toList o of
    [(key, value)]
        | key == left  -> Left  <$> parseJSON value
        | key == right -> Right <$> parseJSON value

It's actually slightly different, since the above will always commit to the pattern Object o when handed an Object, whereas the view pattern will only commit when both the "matches the Object o pattern" and the "H.toList o matches the [(key, value)] pattern" conditions hold, but for this example that doesn't matter.

2
votes

The json package contains an encoding for data types that you might want to adopt. If you just derive Data you can use it. It's not very fast, but very easy to use.

1
votes

Assuming each datatype has a distinct key, another approach could use lenses - I like it because it's concise and is readable. For example if you have a wrapper around an A, B, and C which all have FromJSON instances:

import Data.Aeson
import Data.Maybe
import Data.Aeson.Lens
import Control.Lens

data Wrap = WrapA A | WrapB B | WrapC C

instance FromJSON Wrap where
    parseJSON json
        | isJust (json ^? key "A's unique key") = WrapA <$> parseJSON json
        | isJust (json ^? key "B's unique key") = WrapB <$> parseJSON json
        | isJust (json ^? key "C's unique key") = WrapC <$> parseJSON json
        | otherwise = fail "Bad message"