Suppose I have a JSON like this:
{
data: {...}
}
and {...}
represents a model of mine. How could
get my model in this case in the Handler? For instance, the following will not work obviously:
putMyEntityR :: Handler ()
putMyEntityR = do
(Entity id _) <- (...) -- getting the Key
e <- requireJsonBody :: Handler MyEntity
runDB $ replace id e
sendResponseStatus status204 ("UPDATED" :: Text)
How can I read the JSON
, take the data
object, and only then decode it?
There was some more discussion of this question on a Github Issue, which I'm adding as an answer here because it's more fleshed out. Here's what we arrive at for the Handler function using the helper functions defined below:
postDataCommentR :: Handler Value
postDataCommentR = do
value <- requireJsonBody' -- Parse request body into Value
commentJson <- requireJsonKey "data" value -- Lookup a key from the Value
comment <- (requireJsonParse commentJson :: Handler Comment) -- Parse the Value into a comment record
insertedComment <- runDB $ insertEntity comment
returnJson insertedComment
These functions take the request body and parse it into an aeson Value
:
import qualified Data.Aeson as J
import qualified Data.Aeson.Parser as JP
import Data.Conduit.Attoparsec (sinkParser)
-- These two functions were written by @FtheBuilder
parseJsonBody' :: (MonadHandler m) => m (J.Result Value)
parseJsonBody' = do
eValue <- rawRequestBody $$ runCatchC (sinkParser JP.value')
return $ case eValue of
Left e -> J.Error $ show e
Right value -> J.Success value
-- | Same as 'parseJsonBody', but return an invalid args response on a parse
-- error.
requireJsonBody' :: (MonadHandler m) => m Value
requireJsonBody' = do
ra <- parseJsonBody'
case ra of
J.Error s -> invalidArgs [pack s]
J.Success a -> return a
These helper functions are used to parse that Value
into a record:
requireJsonParse :: (MonadHandler m, FromJSON a) => Value -> m a
requireJsonParse v = case J.fromJSON v of
J.Error s -> invalidArgs [pack s]
J.Success a -> return a
requireJsonKey :: (MonadHandler m) => Text -> Value -> m Value
requireJsonKey key jObject@(Object hashMap) = case lookup key hashMap of
Nothing -> invalidArgs ["Couldn't find a value when looking up the key " <> key <> " in the object: " <> (pack (show jObject))]
Just v -> return v
requireJsonKey key invalid = invalidArgs ["When looking up the key " <> key <> ", expected an object but got a " ++ (pack (show invalid))]
Commentary
aeson-lens
I didn't use aeson-lens
, but the code is pretty similar with or without it, since we're just going one key deep. aeson-lens
would make things nicer if we were traversing deeper into the JSON.
Comparison to wrapper definition
Once you get the helper functions defined, you still have a couple lines to parse a Value
, then lookup the data
key, then create your record. You can do things to make this shorter, but ultimately the wrapper that @Carsten recommended would be of similar length with less complexity, imo.
This was too large to fit in my comment above (using lens-aeson)
λ: import Control.Lens
λ: import Data.Aeson.Lens
λ: let e = "{ \"data\": [1,2,3] }"
λ: e ^? key "data"
Just (Array [Number 1.0,Number 2.0,Number 3.0])
and as Carsten mentioned, you'll still need to provide a FromJSON instance of your model