{-# LANGUAGE CPP, NoImplicitPrelude, TemplateHaskell #-} {-| Module: Data.Aeson.TH License: Apache Stability: experimental Portability: portable Functions to mechanically derive 'ToJSON' and 'FromJSON' instances. Note that you need to enable the @TemplateHaskell@ language extension in order to use this module. An example shows how instances are generated for arbitrary data types. First we define a data type: @ data D a = Nullary | Unary Int | Product String Char a | Record { testOne :: Double , testTwo :: Bool , testThree :: D a } deriving Eq @ Next we derive the necessary instances. Note that we make use of the feature to change record field names. In this case we drop the first 4 characters of every field name. @ $('deriveJSON' ('drop' 4) ''D) @ This will result in the following (simplified) code to be spliced in your program: @ import Control.Applicative import Control.Monad import Data.Aeson import Data.Aeson.TH import qualified Data.Map as M import qualified Data.Text as T import qualified Data.Vector as V instance 'ToJSON' a => 'ToJSON' (D a) where 'toJSON' = \value -> case value of Nullary -> 'object' ['T.pack' \"Nullary\" .= 'toJSON' ([] :: [()])] Unary arg1 -> 'object' ['T.pack' \"Unary\" .= 'toJSON' arg1] Product arg1 arg2 arg3 -> 'object' [ 'T.pack' \"Product\" .= 'toJSON' [ 'toJSON' arg1 , 'toJSON' arg2 , 'toJSON' arg3 ] ] Record arg1 arg2 arg3 -> 'object' [ 'T.pack' \"Record\" .= 'object' [ 'T.pack' \"One\" '.=' arg1 , 'T.pack' \"Two\" '.=' arg2 , 'T.pack' \"Three\" '.=' arg3 ] ] @ @ instance 'FromJSON' a => 'FromJSON' (D a) where 'parseJSON' = \value -> case value of 'Object' obj -> case 'M.toList' obj of [(conKey, conVal)] -> case conKey of _ | (conKey '==' 'T.pack' \"Nullary\") -> case conVal of 'Array' arr | 'V.null' arr -> 'pure' Nullary _ -> 'mzero' | (conKey '==' 'T.pack' \"Unary\") -> case conVal of arg -> Unary '<$>' 'parseJSON' arg | (conKey '==' 'T.pack' \"Product\") -> case conVal of 'Array' arr | 'V.length' arr '==' 3 -> 'Product' '<$>' 'parseJSON' (arr 'V.!' 0) '<*>' 'parseJSON' (arr 'V.!' 1) '<*>' 'parseJSON' (arr 'V.!' 2) _ -> 'mzero' | (conKey '==' 'T.pack' \"Record\") -> case conVal of 'Object' obj -> Record '<$>' (obj '.:' 'T.pack' \"One\") '<*>' (obj '.:' 'T.pack' \"Two\") '<*>' (obj '.:' 'T.pack' \"Three\") _ -> 'mzero' | 'otherwise' -> 'mzero' _ -> 'mzero' _ -> 'mzero' @ Now we can use the newly created instances. @ d :: D 'Int' d = Record { testOne = 3.14159 , testTwo = 'True' , testThree = Product \"test\" \'A\' 123 } @ >>> fromJSON (toJSON d) == Success d > True -} module Data.Aeson.TH ( deriveJSON , deriveToJSON , deriveFromJSON , mkToJSON , mkParseJSON ) where -------------------------------------------------------------------------------- -- Imports -------------------------------------------------------------------------------- -- from aeson: import Data.Aeson ( toJSON, object, (.=), (.:) , ToJSON, toJSON , FromJSON, parseJSON ) import Data.Aeson.Types ( Value(..) ) -- from base: import Control.Applicative ( pure, (<$>), (<*>) ) import Control.Monad ( return, mapM, mzero, liftM2 ) import Data.Bool ( otherwise ) import Data.Eq ( (==) ) import Data.Function ( ($), (.), id ) import Data.Functor ( fmap ) import Data.List ( (++), foldl', map, zip, genericLength ) import Prelude ( String, (-), Integer, error ) import Text.Show ( show ) #if __GLASGOW_HASKELL__ < 700 import Control.Monad ( (>>=), fail ) import Prelude ( fromInteger ) #endif -- from containers: import qualified Data.Map as M ( toList ) -- from template-haskell: import Language.Haskell.TH -- from text: import qualified Data.Text as T ( pack ) -- from vector: import qualified Data.Vector as V ( (!), null, length ) -------------------------------------------------------------------------------- -- Convenience -------------------------------------------------------------------------------- -- | Generates both 'ToJSON' and 'FromJSON' instance declarations for the given -- data type. -- -- This is a convienience function which is equivalent to calling both -- 'deriveToJSON' and 'deriveFromJSON'. deriveJSON :: (String -> String) -- ^ Function to change field names. -> Name -- ^ Name of the type for which to generate 'ToJSON' and 'FromJSON' -- instances. -> Q [Dec] deriveJSON withField name = liftM2 (++) (deriveToJSON withField name) (deriveFromJSON withField name) -------------------------------------------------------------------------------- -- ToJSON -------------------------------------------------------------------------------- {- TODO: Don't constrain phantom type variables. data Foo a = Foo Int instance (ToJSON a) ⇒ ToJSON Foo where ... The above (ToJSON a) constraint is not necessary and perhaps undesirable. -} -- | Generates a 'ToJSON' instance declaration for the given data type. -- -- Example: -- -- @ -- data Foo = Foo 'Char' 'Int' -- $('deriveToJSON' 'id' ''Foo) -- @ -- -- This will splice in the following code: -- -- @ -- instance 'ToJSON' Foo where -- 'toJSON' = -- \value -> case value of -- Foo arg1 arg2 -> 'toJSON' ['toJSON' arg1, 'toJSON' arg2] -- @ deriveToJSON :: (String -> String) -- ^ Function to change field names. -> Name -- ^ Name of the type for which to generate a 'ToJSON' instance -- declaration. -> Q [Dec] deriveToJSON withField name = withType name $ \tvbs cons -> fmap (:[]) $ fromCons tvbs cons where fromCons :: [TyVarBndr] -> [Con] -> Q Dec fromCons tvbs cons = instanceD (return $ map (\t -> ClassP ''ToJSON [VarT t]) typeNames) (classType `appT` instanceType) [ funD 'toJSON [ clause [] (normalB $ consToJSON withField cons) [] ] ] where classType = conT ''ToJSON typeNames = map tvbName tvbs instanceType = foldl' appT (conT name) $ map varT typeNames -- | Generates a lambda expression which encodes the given data type as JSON. -- -- Example: -- -- @ -- data Foo = Foo 'Int' -- @ -- -- @ -- encodeFoo :: Foo -> 'Value' -- encodeFoo = $('mkToJSON' 'id' ''Foo) -- @ -- -- This will splice in the following code: -- -- @ -- \value -> case value of Foo arg1 -> 'toJSON' arg1 -- @ mkToJSON :: (String -> String) -- ^ Function to change field names. -> Name -- ^ Name of the type to encode. -> Q Exp mkToJSON withField name = withType name (\_ cons -> consToJSON withField cons) -- | Helper function used by both 'deriveToJSON' and 'mkToJSON'. Generates code -- to generate the JSON encoding of a number of constructors. All constructors -- must be from the same type. consToJSON :: (String -> String) -- ^ Function to change field names. -> [Con] -- ^ Constructors for which to generate JSON generating code. -> Q Exp consToJSON _ [] = error $ "Data.Aeson.TH.consToJSON: " ++ "Not a single constructor given!" -- A single constructor is directly encoded. The constructor itself may be -- forgotten. consToJSON withField [con] = do value <- newName "value" lam1E (varP value) $ caseE (varE value) [encodeArgs id withField con] -- With multiple constructors we need to remember which constructor is -- encoded. This is done by generating a JSON object which maps to constructor's -- name to the JSON encoding of its contents. consToJSON withField cons = do value <- newName "value" lam1E (varP value) $ caseE (varE value) [ encodeArgs (wrap $ getConName con) withField con | con <- cons ] where wrap :: Name -> Q Exp -> Q Exp wrap name exp = let fieldName = [e|T.pack|] `appE` litE (stringL $ nameBase name) in [e|object|] `appE` listE [ infixApp fieldName [e|(.=)|] exp ] -- | Generates code to generate the JSON encoding of a single constructor. encodeArgs :: (Q Exp -> Q Exp) -> (String -> String) -> Con -> Q Match -- Nullary constructors. Generates code that explicitly matches against the -- constructor even though it doesn't contain data. This is useful to prevent -- type errors. encodeArgs withExp _ (NormalC conName []) = match (conP conName []) (normalB $ withExp [e|toJSON ([] :: [()])|]) [] -- Polyadic constructors with special case for unary constructors. encodeArgs withExp _ (NormalC conName ts) = do args <- mapM newName ["arg" ++ show n | (_, n) <- zip ts [1 :: Integer ..]] let js = case [[e|toJSON|] `appE` varE arg | arg <- args] of -- Single argument is directly converted. [e] -> e -- Multiple arguments are converted to a JSON array. es -> [e|toJSON|] `appE` listE es match (conP conName $ map varP args) (normalB $ withExp js) [] -- Records. encodeArgs withExp withField (RecC conName ts) = do args <- mapM newName ["arg" ++ show n | (_, n) <- zip ts [1 :: Integer ..]] let js = [ infixApp ([e|T.pack|] `appE` fieldNameExp withField field) [e|(.=)|] (varE arg) | (arg, (field, _, _)) <- zip args ts ] match (conP conName $ map varP args) (normalB $ withExp $ [e|object|] `appE` listE js) [] -- Infix constructors. encodeArgs withExp _ (InfixC _ conName _) = do al <- newName "argL" ar <- newName "argR" match (infixP (varP al) conName (varP ar)) ( normalB $ withExp $ [e|toJSON|] `appE` listE [ [e|toJSON|] `appE` varE a | a <- [al,ar] ] ) [] -- Existentially quantified constructors. encodeArgs withExp withField (ForallC _ _ con) = encodeArgs withExp withField con -------------------------------------------------------------------------------- -- FromJSON -------------------------------------------------------------------------------- -- | Generates a 'FromJSON' instance declaration for the given data type. -- -- Example: -- -- @ -- data Foo = Foo 'Char' 'Int' -- $('deriveFromJSON' 'id' ''Foo) -- @ -- -- This will splice in the following code: -- -- @ -- instance 'FromJSON' Foo where -- 'parseJSON' = -- \value -> case value of -- 'Array' arr | ('V.length' arr '==' 2) -> -- Foo '<$>' 'parseJSON' (arr 'V.!' 0) -- '<*>' 'parseJSON' (arr 'V.!' 1) -- _ -> 'mzero' -- @ deriveFromJSON :: (String -> String) -- ^ Function to change field names. -> Name -- ^ Name of the type for which to generate a 'FromJSON' instance -- declaration. -> Q [Dec] deriveFromJSON withField name = withType name $ \tvbs cons -> fmap (:[]) $ fromCons tvbs cons where fromCons :: [TyVarBndr] -> [Con] -> Q Dec fromCons tvbs cons = instanceD (return $ map (\t -> ClassP ''FromJSON [VarT t]) typeNames) (classType `appT` instanceType) [ funD 'parseJSON [ clause [] (normalB $ consFromJSON withField cons) [] ] ] where classType = conT ''FromJSON typeNames = map tvbName tvbs instanceType = foldl' appT (conT name) $ map varT typeNames -- | Generates a lambda expression which parses the JSON encoding of the given -- data type. -- -- Example: -- -- @ -- data Foo = Foo 'Int' -- @ -- -- @ -- parseFoo :: 'Value' -> 'Parser' Foo -- parseFoo = $('mkParseJSON' 'id' ''Foo) -- @ -- -- This will splice in the following code: -- -- @ -- \\value -> case value of arg -> Foo '<$>' 'parseJSON' arg -- @ mkParseJSON :: (String -> String) -- ^ Function to change field names. -> Name -- ^ Name of the encoded type. -> Q Exp mkParseJSON withField name = withType name (\_ cons -> consFromJSON withField cons) -- | Helper function used by both 'deriveFromJSON' and 'mkParseJSON'. Generates -- code to parse the JSON encoding of a number of constructors. All constructors -- must be from the same type. consFromJSON :: (String -> String) -- ^ Function to change field names. -> [Con] -- ^ Constructors for which to generate JSON parsing code. -> Q Exp consFromJSON _ [] = error $ "Data.Aeson.TH.consFromJSON: " ++ "Not a single constructor given!" consFromJSON withField [con] = do value <- newName "value" lam1E (varP value) $ caseE (varE value) (parseArgs withField con) consFromJSON withField cons = do value <- newName "value" obj <- newName "obj" conKey <- newName "conKey" conVal <- newName "conVal" let -- Convert the Data.Map inside the Object to a list and pattern match -- against it. It must contain a single element otherwise the parse will -- fail. caseLst = caseE ([e|M.toList|] `appE` varE obj) [ match (listP [tupP [varP conKey, varP conVal]]) (normalB caseKey) [] , errorMatch ] caseKey = caseE (varE conKey) [match wildP (guardedB guards) []] guards = [ do g <- normalG $ infixApp (varE conKey) [|(==)|] ( [|T.pack|] `appE` conNameExp con ) e <- caseE (varE conVal) (parseArgs withField con) return (g, e) | con <- cons ] ++ [liftM2 (,) (normalG [e|otherwise|]) [e|mzero|]] lam1E (varP value) $ caseE (varE value) [ match (conP 'Object [varP obj]) (normalB caseLst) [] , errorMatch ] where -- Makes a string literal expression from a constructor's name. conNameExp :: Con -> Q Exp conNameExp = litE . stringL . nameBase . getConName -- | Generates code to parse the JSON encoding of a single -- constructor. parseArgs :: (String -> String) -- ^ Function to change field names. -> Con -- ^ Constructor for which to generate JSON parsing code. -> [Q Match] -- Nullary constructors. parseArgs _ (NormalC conName []) = [ do arr <- newName "arr" g <- normalG $ [|V.null|] `appE` varE arr e <- [e|pure|] `appE` conE conName -- TODO: Use applicative style: guardedB [(,) <$> g' <*> e'] -- But first need to have "instance Applicative Q". match (conP 'Array [varP arr]) (guardedB [return (g, e)]) [] , errorMatch ] -- Unary constructors. parseArgs _ (NormalC conName [_]) = [ do arg <- newName "arg" match (varP arg) ( normalB $ infixApp (conE conName) [e|(<$>)|] ([e|parseJSON|] `appE` varE arg) ) [] ] -- Polyadic constructors. parseArgs _ (NormalC conName ts) = parseProduct conName $ genericLength ts -- Records. parseArgs withField (RecC conName ts) = [ do obj <- newName "obj" -- List of: "obj .: "<FIELD>"" let x:xs = [ infixApp (varE obj) [|(.:)|] ( [e|T.pack|] `appE` fieldNameExp withField field ) | (field, _, _) <- ts ] match (conP 'Object [varP obj]) ( normalB $ foldl' (\a b -> infixApp a [|(<*>)|] b) (infixApp (conE conName) [|(<$>)|] x) xs ) [] , errorMatch ] -- Infix constructors. Apart from syntax these are the same as -- polyadic constructors. parseArgs _ (InfixC _ conName _) = parseProduct conName 2 -- Existentially quantified constructors. We ignore the quantifiers -- and proceed with the contained constructor. parseArgs withField (ForallC _ _ con) = parseArgs withField con -- | Generates code to parse the JSON encoding of an n-ary -- constructor. parseProduct :: Name -- ^ 'Con'structor name. -> Integer -- ^ 'Con'structor arity. -> [Q Match] parseProduct conName numArgs = [ do arr <- newName "arr" g <- normalG $ infixApp ([|V.length|] `appE` varE arr) [|(==)|] (litE $ integerL numArgs) -- List of: "parseJSON (arr V.! <IX>)" let x:xs = [ [|parseJSON|] `appE` infixApp (varE arr) [|(V.!)|] (litE $ integerL ix) | ix <- [0 .. numArgs - 1] ] e <- foldl' (\a b -> infixApp a [|(<*>)|] b) (infixApp (conE conName) [|(<$>)|] x) xs match (conP 'Array [varP arr]) (guardedB [return (g, e)]) [] , errorMatch ] -- | -- @ -- _ -> 'mzero' -- @ errorMatch :: Q Match errorMatch = match wildP (normalB [|mzero|]) [] -------------------------------------------------------------------------------- -- Utility functions -------------------------------------------------------------------------------- -- | Boilerplate for top level splices. -- -- The given 'Name' must be from a type constructor. Furthermore, the -- type constructor must be either a data type or a newtype. Any other -- value will result in an exception. withType :: Name -> ([TyVarBndr] -> [Con] -> Q a) -- ^ Function that generates the actual code. Will be applied -- to the type variable binders and constructors extracted -- from the given 'Name'. -> Q a -- ^ Resulting value in the 'Q'uasi monad. withType name f = do info <- reify name case info of TyConI dec -> case dec of DataD _ _ tvbs cons _ -> f tvbs cons NewtypeD _ _ tvbs con _ -> f tvbs [con] other -> error $ "Data.Aeson.TH.withType: Unsupported type: " ++ show other _ -> error "Data.Aeson.TH.withType: I need the name of a type." -- | Extracts the name from a constructor. getConName :: Con -> Name getConName (NormalC name _) = name getConName (RecC name _) = name getConName (InfixC _ name _) = name getConName (ForallC _ _ con) = getConName con -- | Extracts the name from a type variable binder. tvbName :: TyVarBndr -> Name tvbName (PlainTV name ) = name tvbName (KindedTV name _) = name -- | Creates a string literal expression from a record field name. fieldNameExp :: (String -> String) -- ^ Function to change the field name. -> Name -> Q Exp fieldNameExp f = litE . stringL . f . nameBase