I don't know of a good strategy to get where you want to be since the ParseJSON monad is not a transformer or based on IO. What you can more easily do is decode into one type then translate into the second as done in a prior question 'Give a default value for fields not available in json using aeson'.
Since large structures can be cumbersome to reproduce you could make the structure parameterized and instantiate it with either IO Int
or Int
. For example, let's say you wanted field a
from the wire but b
as random from the IO monad:
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveTraversable #-}
import Data.Aeson
import System.Random
import Data.ByteString.Lazy (ByteString)
data Example' a =
Example { a :: Int
, b :: a
} deriving (Show,Functor,Foldable,Traversable)
type Partial = Example' (IO Int)
type Example = Example' Int
instance FromJSON Partial where
parseJSON (Object o) =
Example <$> o .: "a"
<*> pure (randomRIO (1,10))
loadExample :: Partial -> IO Example
loadExample = mapM id
parseExample :: ByteString -> IO (Maybe Example)
parseExample = maybe (pure Nothing) (fmap Just . loadExample) . decode
Notice how loadExample
uses our traverse
instance to execute the IO actions inside the structure. Here is an example use:
Main> parseExample "{ \"a\" : 1111 }"
Just (Example {a = 1111, b = 5})
Advanced
If you had more than one type of field for which you wanted an IO action you could either
Make one data type for all of them. Instead of b
being type IO Int
you might make it IO MyComplexRecord
. This is the easy solution.
The more complex and fun solution is to use a higher kind type parameter.
For option 2, consider:
data Example' f = Example { a :: Int
, b :: f Int
, c :: f String }
You could then use Proxy
and Control.Monad.Identity
instead of values like IO Int
and Int
used previously. You'll need to write your own traversal since you can't derive Traverse
for this class (which is what gives us the mapM
used above). We could make a traversal class with kind (* -> *) -> *
using a few extensions (RankNTypes among them) but unless this is done often, and we get some sort of deriving support or TH, I don't think that is worthwhile.