Search code examples
jsonhaskellaeson

JSON nested serialisation


Assume there is a data type

data V = V { a :: Int, x :: Int, y :: Int }

It has a correspondent JSON view

E.g. V { a = 1, x = 2, y = 3 } need to be serialised like

{
  "a": 1,
  "nested": {
    "x": 2,
    "y": 3
  }
}

What ToJSON instance would look like in that case?


What I've tried:

instance ToJSON V where
  toEncoding (V a b c) = 
    pairs (  "a" .= a
          <> ("nested" .= pairs ("x" .= x <> "y" .= y))
          )


<interactive>:6:10: error:
    • No instance for (GHC.Generics.Generic V)
        arising from a use of ‘aeson-1.1.1.0:Data.Aeson.Types.ToJSON.$dmtoJSON’
    • In the expression:
        aeson-1.1.1.0:Data.Aeson.Types.ToJSON.$dmtoJSON @V
      In an equation for ‘toJSON’:
          toJSON = aeson-1.1.1.0:Data.Aeson.Types.ToJSON.$dmtoJSON @V
      In the instance declaration for ‘ToJSON V’

<interactive>:6:68: error:
    • No instance for (ToJSON Encoding) arising from a use of ‘.=’
    • In the second argument of ‘(<>)’, namely
        ‘("nested" .= pairs ("x" .= x <> "y" .= y))’
      In the first argument of ‘pairs’, namely
        ‘("a" .= a <> ("nested" .= pairs ("x" .= x <> "y" .= y)))’
      In the expression:
        pairs ("a" .= a <> ("nested" .= pairs ("x" .= x <> "y" .= y)))

<interactive>:6:87: error:
    • No instance for (ToJSON (V -> Int)) arising from a use of ‘.=’
        (maybe you haven't applied a function to enough arguments?)
    • In the first argument of ‘(<>)’, namely ‘"x" .= x’
      In the first argument of ‘pairs’, namely ‘("x" .= x <> "y" .= y)’
      In the second argument of ‘(.=)’, namely
        ‘pairs ("x" .= x <> "y" .= y)’
(0.01 secs,)

Solution

  • Here how instance may look like:

    data V = V { a :: Int, x :: Int, y :: Int }
    
    instance ToJSON V where
        toJSON (V a x y) = object
            [ "a" .= a
            , "nested" .= object
                [ "x" .= x
                , "y" .= y ]
            ]
    

    You can test it in ghci:

    ghci> import qualified Data.ByteString.Lazy.Char8 as B
    ghci> B.putStrLn $ encode (V 1 2 3)
    {"nested":{"x":2,"y":3},"a":1}
    

    UPD (regarding toEncoding):

    You most likely don't want to define toEncoding. This method has default implementation and it's defined using toJSON method. But toJSON method has no implementation for general case. It has only default implementation for Generic data types.

    Your implementation if almost fine except it has typo: "x" .= x <> "y" .= y in method body and (V a b c) in pattern matching (thus it uses x variable as function and you got those creepy errors). And you need to derive Generic for your V data type for this to work. And you need to use pair function from internals instead of .= in one place. Here is full version:

    {-# LANGUAGE OverloadedStrings #-}
    {-# LANGUAGE DeriveGeneric #-}
    
    import Data.Monoid ((<>))
    import GHC.Generics (Generic)
    import Data.Aeson (ToJSON (..), pairs, (.=))
    import Data.Aeson.Encoding.Internal (pair)
    
    data V = V { a :: Int, x :: Int, y :: Int } deriving (Generic)
    
    instance ToJSON V where
        toEncoding (V a x y) = 
            pairs ("a" .= a <> (pair "nested" $ pairs ("x" .= x <> "y" .= y)))
    

    But be aware of possible inconsistency:

    ghci> encode (V 1 2 3)
    "{\"a\":1,\"nested\":{\"x\":2,\"y\":3}}"
    ghci> toEncoding (V 1 2 3)
    "{\"a\":1,\"nested\":{\"x\":2,\"y\":3}}"
    ghci> toJSON (V 1 2 3)
    Object (fromList [("a",Number 1.0),("x",Number 2.0),("y",Number 3.0)])