Search code examples
jsonhaskelltypeclassaeson

Trouble type-checking Aeson decoding into generic types


This is my first attempt at JSON deserialising with Aeson. I'm having trouble to type-check a generic decoding function for all my domain data types, even though a corresponding decoding function for a single concrete type does work.

Here is the polymorphic function:

import qualified RIO.ByteString.Lazy           as BL
import qualified Data.Aeson                    as J
import qualified Path.Posix                    as P

loadDomainData :: J.FromJSON dData => FC.AbsFilePath -> IO dData
loadDomainData filePath = do
    fileContents <- readFileBinary $ P.toFilePath filePath
    let
        decData :: Maybe dData
        decData = J.decode $ BL.fromStrict fileContents
    case decData of
        Just d -> return d
        Nothing -> throwString ("Could not decode data file " <> P.toFilePath filePath)

After initial failures, I inserted a type annotation for the target type of the decoder, but to no avail. If I try to compile it, the following type-check error results:

    • Could not deduce (J.FromJSON dData1)
        arising from a use of ‘J.decode’
      from the context: J.FromJSON dData
        bound by the type signature for:
                   loadDomainData :: forall dData.
                                     J.FromJSON dData =>
                                     FC.AbsFilePath -> IO dData
        at src/Persistence/File/ParticipantRepository.hs:44:1-64
      Possible fix:
        add (J.FromJSON dData1) to the context of
          the type signature for:
            decData :: forall dData1. Maybe dData1
    • In the expression: J.decode $ BL.fromStrict fileContents
[..]

What am I missing? Thanks four any insight!


Solution

  • You shouldn't need the type annotation at all. Does it not compile fine without it?

    What you are missing is that variables in a type signature in a let or where clause are not scoped within the type signature of the containing function. So, the type variable dData in the signature for loadDomainData is completely unrelated to the dData in the signature for decData. GHC is complaining that the type in decData has no J.FromJSON instance because the type signature says it doesn't have one. You can add it:

    decData :: J.FromJSON dData => Maybe dData
    

    or you can turn on the ScopedTypeVariables extension and modify the type signature for the containing function to mark the dData variable as scoped:

    loadDomainData :: forall dData. J.FromJSON dData => FilePath -> IO dData
    

    while keeping the same decData declaration as before (no forall and no constraint):

    decData :: Maybe dData
    

    or, as mentioned above, you can remove the type signature for decData entirely. So, all three of the following should work:

    {-# LANGUAGE ScopedTypeVariables #-}
    
    -- Add constraint to `decData` signature
    loadDomainData :: J.FromJSON dData => FC.AbsFilePath -> IO dData
    loadDomainData filePath = do
        fileContents <- readFileBinary $ P.toFilePath filePath
        let
            decData :: J.FromJSON dData => Maybe dData
            decData = J.decode $ BL.fromStrict fileContents
        case decData of
            Just d -> return d
            Nothing -> throwString ("Could not decode data file " <> P.toFilePath filePath)
    
    -- Use ScopedTypeVariables
    loadDomainData :: forall dData. J.FromJSON dData => FC.AbsFilePath -> IO dData
    loadDomainData filePath = do
        fileContents <- readFileBinary $ P.toFilePath filePath
        let
            decData :: Maybe dData
            decData = J.decode $ BL.fromStrict fileContents
        case decData of
            Just d -> return d
            Nothing -> throwString ("Could not decode data file " <> P.toFilePath filePath)
    
    -- No `decData` signature
    loadDomainData :: J.FromJSON dData => FC.AbsFilePath -> IO dData
    loadDomainData filePath = do
        fileContents <- readFileBinary $ P.toFilePath filePath
        let
            decData = J.decode $ BL.fromStrict fileContents
        case decData of
            Just d -> return d
            Nothing -> throwString ("Could not decode data file " <> P.toFilePath filePath)