Search code examples
jsonhaskellaeson

Aeson parse JSON with default value from previous parse


I want to parse the following json:

{
  "defaults": {
    "align": "left"
  },
  "animals": [
    {
      "kind": "cat",
      "name": "Oscar",
      "align": "center"
    },
    {
      "kind": "dog",
      "name": "Max"
    }
  ]
}

Parse align:

data Align = Left | Center | Right

instance FromJSON Align where
  parseJSON (String "left")   = pure Left
  parseJSON (String "center") = pure Center
  parseJSON (String "right")  = pure Right
  parseJSON _ = fail "Expect one of [left, center, right]."

Parse defaults:

data BlockDefaults = BlockDefaults { align :: Align }

-- default value Center if key does not exist
blockDefaults :: BlockDefaults
blockDefaults = BlockDefaults { align = Center }

instance FromJSON BlockDefaults where
  parseJSON = withObject "defaults" $ \o -> BlockDefaults <$> o .:? "align" .!= align blockDefaults

Now I want to parse dog and cat. If align does not exist (as in dog), it should take the value from defaults (left). So dog should become Dog{name="Max", align=Center} and cat Cat{name="Oscar", align=Left}.

But how do I access the default align value in parseJSON?

-- pseudo parse code
instance FromJSON Animal where
  parseJSON = withObject "animal" $ \o ->
    Animal <$>
    o .: "kind" <*>
    o .: "name" <*>
    o .:? "align" .!= <DefaultValue> -- How to access value from defaults object?

I do not want to parse defaults for every animal again, so how can I access those defaults values parsed before? Assume there are much more values in defaults and other animals.


So the animal parser code looks like this now:

parseAnimal :: BlockDefaults -> Value -> Parser Animal
parseAnimal defaults = withObject "animal" $ \o ->
  Animal <$> 
    o .: "kind" <*>
    o .: "name" <*>
    (
      BlockDefaults <$>
      o .:? "align" .!= align defaults
    )

Solution

  • My commented suggestion was to use BlockDefaults -> Animal as the instance. For example:

    #!/usr/bin/env cabal
    {- cabal:
         build-depends: base, aeson
    -}
    {-# LANGUAGE OverloadedStrings #-}
    {-# LANGUAGE FlexibleInstances #-}
    
    import Prelude hiding (Either(..)) -- This is why you don't name your constructors 'Left' and 'Right'
    import Data.Aeson
    
    data Align = Left | Center | Right
        deriving (Show)
    
    instance FromJSON Align where
      parseJSON (String "left")   = pure Left
      parseJSON (String "center") = pure Center
      parseJSON (String "right")  = pure Right
      parseJSON _ = fail "Expect one of [left, center, right]."
    
    data BlockDefaults = BlockDefaults { align :: Align }
    
    -- default value Center if key does not exist
    blockDefaults :: BlockDefaults
    blockDefaults = BlockDefaults { align = Center }
    
    instance FromJSON BlockDefaults where
      parseJSON = withObject "defaults" $ \o -> BlockDefaults <$> o .:? "align" .!= align blockDefaults
    
    data Animal = Animal { kind   :: String
                         , name   :: String
                         , aalign :: Align
                         }
        deriving (Show)
    
    instance FromJSON (BlockDefaults -> Animal) where
        parseJSON = withObject "Animal" $ \o ->
            do k <- o  .: "kind"
               n <- o  .: "name"
               a <- o .:? "align"
               pure $ \def -> Animal k n (maybe (align def) id a)
    
    main :: IO ()
    main = print (decode "{ \"kind\": \"k\", \"name\" : \"n\" }" <*> Just blockDefaults :: Maybe Animal)