Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Write json to a file in Haskell (with Text rather than [Char])

Tags:

json

haskell

I'm trying to serialize an object into a JSON string and write it to a file.

In python, I'd do something like:

>>> meowmers = {"name" : "meowmers", "age" : 1}
>>> import json
>>> with open("myfile.json","wb") as f
    json.dump(meowmers, f)

$ cat myfile.json
{"age": 1, "name": "meowmers"}

I'm looking at this in Haskell

$ stack ghci

{-# LANGUAGE OverloadedStrings #-}
:set -XOverloadedStrings

import GHC.Generics
import Data.Aeson as A
import Data.Text.Lazy as T
import Data.Text.Lazy.IO as I

:{
data Cat = Cat {
      name :: Text
    , age  :: Int
    } deriving Show
:}

let meowmers = Cat {name = "meowmers", age = 1}
writeFile "myfile.json" (encode meowmers)

Oh no!

*A T I GHC.Generics> I.writeFile "myfile2.json" (encode meowmers)

<interactive>:34:29:
    Couldn't match expected type ‘Text’
                with actual type ‘bytestring-0.10.6.0:Data.ByteString.Lazy.Internal.ByteString’
    In the second argument of ‘I.writeFile’, namely ‘(encode meowmers)’
    In the expression: I.writeFile "myfile2.json" (encode meowmers)

Two questions:

  1. This appears to be a bytestring. How can I work with that?
  2. If that's not what I want to do, is there a Haskell json serialization solution using Text rather than String that is yet rather simple?
like image 442
Mittenchops Avatar asked Aug 23 '16 00:08

Mittenchops


Video Answer


2 Answers

So, to iron everything out (since most of the work has already been done). You actually have two problems:

  1. You are mixing string types
  2. You don't have an instance of ToJSON declared for Cat

Here is a working example that relies on recent versions of aeson and text (for me that is aeson-1.0.0.0 and text-1.2.2.1.

{-# LANGUAGE OverloadedStrings, DeriveGeneric, DeriveAnyClass #-}

import GHC.Generics
import Data.Text.Lazy (Text)
import Data.Text.Lazy.IO as I
import Data.Aeson.Text (encodeToLazyText)
import Data.Aeson (ToJSON)

data Cat = Cat { name :: Text, age :: Int } deriving (Show, Generic, ToJSON)

meowmers = Cat { name = "meowmers", age = 1 }

main = I.writeFile "myfile.json" (encodeToLazyText meowmers)

As you can probably tell from the imports, I rely on aeson to convert between string types through encodeToLazyText. That deals with problem number 1.

Then, I use the language extension DeriveGeneric to get a Generic instance for Cat, and use that in conjunction with the extension DeriveAnyClass to get an instance of ToJSON for Cat. The magic of that instance is again part of aeson.

Running this, I get a new file myfile.json that contains {"age":1,"name":"meowmers"} in it.

like image 124
Alec Avatar answered Oct 08 '22 12:10

Alec


You can encode JSON to a lazy Text value directly using Data.Aeson.Text.encodeToLazyText.

{-# LANGUAGE DeriveGeneric #-}

import Data.Aeson.Text (encodeToLazyText)

...

I.writeFile "myfile.json" (encodeToLazyText meowmers)

A bytestring is a type for binary data—not necessarily text. To represent textual data in a bytestring, you need to encode it with some encoding like UTF-8. Once you have a bytestring (encoded with UTF-8 or whatever format makes sense), you can write it to a file using Data.ByteString functions:

import qualified Data.ByteString.Lazy as BS

BS.writeFile "myfile.json" (encode meowmers)

To make this work you need to give your Cat type a ToJSON instance that specifies how to encode it in JSON. You can do this automatically with the DeriveGeneric extension:

data Cat = Cat { ... } deriving (Show, Generic)

instance ToJSON Cat

You can also do this manually if you need finer control over what the resulting JSON looks like.

like image 43
Tikhon Jelvis Avatar answered Oct 08 '22 13:10

Tikhon Jelvis