1

I need to serialize a record in Haskell, and am trying to do it with Aeson. The problem is that some of the fields are ByteStrings, and I can't work out from the examples how to encode them. My idea is to first convert them to text via base64. Here is what I have so far (I put 'undefined' where I didn't know what to do):

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where

import qualified Data.Aeson as J
import qualified Data.ByteString as B
import qualified Data.ByteString.Base64 as B64
import qualified Data.Text as T
import qualified Data.Text.Encoding as E
import qualified GHC.Generics as G

data Data = Data
  { number :: Int
  , bytestring :: B.ByteString
  } deriving (G.Generic, Show)

instance J.ToJSON Data where
  toEncoding = J.genericToEncoding J.defaultOptions

instance J.FromJSON Data

instance J.FromJSON B.ByteString where
  parseJSON = undefined

instance J.ToJSON B.ByteString where
  toJSON = undefined

byteStringToText :: B.ByteString -> T.Text
byteStringToText = E.decodeUtf8 . B64.encode

textToByteString :: T.Text -> B.ByteString
textToByteString txt =
  case B64.decode . E.encodeUtf8 $ txt of
    Left err -> error err
    Right bs -> bs

encodeDecode :: Data -> Maybe Data
encodeDecode = J.decode . J.encode

main :: IO ()
main = print $ encodeDecode $ Data 1 "A bytestring"

It would be good if it was not necessary to manually define new instances of ToJSON and FromJSON for every record, because I have quite a few different records with bytestrings in them.

8n8
  • 1,233
  • 1
  • 8
  • 21

1 Answers1

3

parseJson needs to return a value of type Parser B.ByteString, so you just need to call pure on the return value of B64.decode.

import Control.Monad

-- Generalized to any MonadPlus instance, not just Either String
textToByteString :: MonadPlus m =>  T.Text -> m B.ByteString
textToByteString = case B64.decode (E.encodeUtf8 x) of
                     Left _ -> mzero
                     Right bs -> pure bs

instance J.FromJSON B.ByteString where
  parseJSON (J.String x) = textToByteString x
  parseJSON _ = mzero

Here, I've chosen to return mzero both if you try to decode anything other than a JSON string and if there is a problem with the base-64 decoding.

Likewise, toJSON needs just needs to encode the Text value you create from the base64-encoded ByteString.

instance J.ToJSON B.ByteString where
  toJSON = J.toJSON . byteStringToText

You might want to consider using a newtype wrapper instead of defining the ToJSON and FromJSON instances on B.ByteString directly.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • Thanks, it works! Why is it better to use a newtype wrapper for ByteString? – 8n8 Dec 03 '18 at 18:32
  • 2
    Mosty for the same reason you don't define `Monoid` for `Int`, as there are *lots* of ways you can define a monoid over the integers (`+` with 0, `*` with 1`, etc). Likewise, this is *a* way you could encode a `ByteString` value, but it's not the *only* way, so you "localize" the instances to your data type. – chepner Dec 03 '18 at 18:35