Haskell's type classes are good when you know, that you'll ever have only one instance. But sometimes you need to serialize the same structure into different representations. That is exactly the issue you have.
I can propose the next solution: Create type class with two parameters (requires MultiParamTypeClasses
extension). One of them will be the structure you are going to serialize; the second will be a tag to select specific json format. Example:
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
import Data.Aeson
import qualified Data.Vector as Vector
import Data.Text (Text)
import qualified Data.ByteString.Lazy as BSL
-- our custom variant on ToJSON
class ToJSON' tag a where
toJSON' :: tag -> a -> Value
-- instance for lists, requires FlexibleInstances
instance ToJSON' tag a => ToJSON' tag [a] where
toJSON' tag l = Array $ Vector.fromList $ map (toJSON' tag) l
-- our data type
data Test = Test {
testString :: Text,
testBool :: Bool
}
-- the tag for the first json format
data TestToJSON1 = TestToJSON1
-- the first json format definition
instance ToJSON' TestToJSON1 Test where
toJSON' _ test = object [
"string1" .= String (testString test),
"bool1" .= Bool (testBool test)
]
-- the tag for the second json format
data TestToJSON2 = TestToJSON2
-- the second json format definition
instance ToJSON' TestToJSON2 Test where
toJSON' _ test = object [
"string2" .= String (testString test),
"bool2" .= Bool (testBool test)
]
-- usage example
main :: IO ()
main = do
let test = Test {
testString = "hello",
testBool = False
}
BSL.putStr $ encode $ toJSON' TestToJSON1 test
putStrLn ""
BSL.putStr $ encode $ toJSON' TestToJSON1 [test, test]
putStrLn ""
BSL.putStr $ encode $ toJSON' TestToJSON2 test
putStrLn ""
BSL.putStr $ encode $ toJSON' TestToJSON2 [test, test]
putStrLn ""
The output:
{"string1":"hello","bool1":false}
[{"string1":"hello","bool1":false},{"string1":"hello","bool1":false}]
{"bool2":false,"string2":"hello"}
[{"bool2":false,"string2":"hello"},{"bool2":false,"string2":"hello"}]
That way you need to define one ToJSON'
instance per json format for every data type, and one instance per container (in the example I implemented it only for lists)
If you don't like MultiParamTypeClasses
, you can pass to toJSON'
a function that knows how to serialize your data type.
Note: OverloadedStrings
is not strictly necessary. FlexibleInstances
is already used inside Data.Aeson