Search code examples
jsonhaskellbase64bytestring

Is my ToJSON Base64 instance sane? What would FromJSON instance look like?


I'm trying to encode a Base64 to JSON. I came across this dialogue. I noticed snoyberg has already included the Base64 newtype in the FP Complete codebase, so I thought I would give that a try.

import qualified Data.ByteString as B

import qualified Data.ByteString.Base64 as B64

newtype Base64 = Base64 { toByteString :: B.ByteString }
deriving ( Eq, Ord, Show,IsString)

The ToJSON instance seemed simple enough. I'd appreciate a sanity check.

instance ToJSON Base64 where toJSON (Base64 bs) =
toJSON $ B.unpack $ B64.decodeLenient bs

The FromJSON instance is where I run into problems.

Studying other examples I surmise I must use withArray which wants a (Array -> Parser a). This is where I get stuck.

parseBase64 :: Array -> Parser

parseBase64 (Array a) = ...

I've tried many approaches here, I'm confused as to what needs to happen here or even if I'm on the right track at all. If I could get some feedback as simple as "you're on the right track keep going", or being pointed in a different direction, that would be appreciated.


Solution

  • I have done this exercise before: https://github.com/futurice/haskell-base64-bytestring-type/blob/0a1176d16c71c219fe113bc3f130f64d8dda47bc/src/Data/ByteString/Base64/Type.hs#L46-L51

    -- | Get base64 encoded bytestring
    getEncodedByteString64 :: ByteString64 -> ByteString
    getEncodedByteString64 = Base64.encode . getByteString64
    
    instance ToJSON ByteString64 where
        toJSON = toJSON . decodeLatin1 . getEncodedByteString64
    
    instance FromJSON ByteString64 where
        parseJSON = withText "ByteString" $
            pure . ByteString64 . decodeLenient . encodeUtf8
    

    After closer look, seems that you encode ByteString (any) as an array of 'Word8', (this what BS.unpack returns) for that you could do, and have an encoded bytestring in Base64:

    instance ToJSON Base64 where
        toJSON = toJSON . B.unpack . B64.decodeLenient . toByteString
    
    instance FromJSON Base64 where
        parseJSON = fmap (Base64 . B64.encode . B.pack) . parseJSON
    

    This way you don't have to worry how [Word8] is encoded, as far as it's decoded accordinly.

    If you like to handle array manually, then it would look like:

    instance FromJSON Base64 where
        parseJSON (Array a) = do
            a' <- traverse parseJSON a  -- :: Parser (V.Vector Word8)
            return $ Base64 . B64.encode . B.pack . V.toList $ a'
        parseJSON _ = fail "Array expected"
    

    or

    instance FromJSON Base64 where
        parseJSON = withArray "Base64" $ \a -> do
            a' <- traverse parseJSON a  -- :: Parser (V.Vector Word8)
            return $ Base64 . B64.encode . B.pack . V.toList $ a'