Converting Data To and From Json in PureScript
In this post I will show you how to manually convert PureScript data into JSON and then decode back into PureScript data. The technical name for this is marshalling. To learn how to automatically derive encoding and decoding code, see Automatically de/encoding JSON in Purescript using Generics-Rep.
Let's start with data types representing the board game Quarto and a type for transferring game state.
data Property
= Hollow
| Solid
| Tall
| Short
| Dark
| Light
| Square
| Circle
type PieceID = String
type PositionID = String
type Piece = StrMap Property
type Board = StrMap (Maybe Piece)
type GameState = {
ondeck :: Maybe PieceID,
board :: Board
}
data Protocol = SendState GameState
The functions I'll be using are located in the following modules.
import Data.Argonaut (
class EncodeJson,
class DecodeJson,
jsonSingletonObject, decodeJson, encodeJson, getField,
fromString, toString)
import Data.Argonaut.Core (jsonEmptyObject, stringify)
import Data.Argonaut.Parser (jsonParser)
import Data.Argonaut.Encode.Combinators ((:=), (~>), assoc, extend)
Let's start with encoding/decoding Property
to and from a string.
instance encodeProperty :: EncodeJson Property where
encodeJson r = fromString $ case r of
Hollow -> "hollow"
Solid -> "solid"
Tall -> "tall"
Short -> "short"
Dark -> "dark"
Light -> "light"
Square -> "square"
Circle -> "circle"
instance decodeProperty :: DecodeJson Property where
decodeJson s = do
a <- maybeFail "Could not read Property string." $ toString s
case a of
"hollow" -> Right Hollow
"solid" -> Right Solid
"tall" -> Right Tall
"short" -> Right Short
"dark" -> Right Dark
"light" -> Right Light
"square" -> Right Square
"circle" -> Right Circle
_ -> Left $ "Property string was not one of the" <>
"expected values. Value was '" <> a <> "'"
Note that maybeFail
is a utility function I use to explicitly set the error message if Argonaut fails with a Nothing
. This is really
helpful when tracking down why encoding/decoding isn't working.
maybeFail :: forall a. String -> Maybe a -> Either String a
maybeFail s m = case m of
Just mm -> Right mm
Nothing -> Left s
The encode/decode instances for Property
are all we need in order to encode/decode Piece
and Board
, since Argonaut provides EncodeJson
and DecodeJson
instances for StrMap
and Maybe
. So all that's left is to create instances for the Protocol
type.
-- note (:=) is the same as `assoc`
-- and (~>) is the same as `extend`
instance encodeProtocol :: EncodeJson Protocol where
encodeJson (SendState gs) = jsonSingletonObject "state"
("ondeck" := gs.ondeck
~> "board" := gs.board
~> jsonEmptyObject
)
instance decodeProtocol :: DecodeJson Protocol where
decodeJson json = do
obj <- decodeJson json
state <- getField obj "state"
board <- getField state "board"
ondeck <- getField state "ondeck"
pure $ SendState { ondeck, board }
This will allows us to convert Protocol
into the json object { state : { board : {}, ondeck : {}}}
.
Finally, to illustrate how you would use these instances (or rather how they're used behind the scenes) these two functions encode/decode the Protocol to and from text. These would be used with your API code for marshalling server requests and responses.
protocolToString :: Protocol -> String
protocolToString = stringify <<< encodeJson
stringToProtocol :: String -> Either String Protocol
stringToProtocol string = do
ss <- jsonParser string
decodeJson ss
For PureScript 0.10.x
Note: Below this point is content from the previous version of this post. Code examples are for PureScript 0.10.x ecosystem but the ideas are still applicable.
purescript-argonaut-codecs
provides type classes for both encoding and decoding to JSON. If you look at the code for DecodeJson
, you'll see there are already an type class instance to convert a StrMap a
and a Maybe a
into an a
.
class DecodeJson a where
decodeJson :: Json -> Either String a
instance decodeStrMap :: DecodeJson a => DecodeJson (StrMap a)
instance decodeJsonMaybe :: DecodeJson a => DecodeJson (Maybe a)
Since Board
is an StrMap
of Maybe Piece
, and Piece
is an StrMap
of Property
, all we to do is create an instance of DecodeJson
for Property
, and we'll have a way to automatically decode both a Piece
and a Board
. Similarly, EncodeJson
provided instances for StrMap
and Maybe
, so we just needed to create an instance of EncodeJson
for Property
.
class EncodeJson a where
encodeJson :: a -> Json
instance encodeStrMap :: EncodeJson a => EncodeJson (StrMap a)
instance encodeJsonMaybe :: EncodeJson a => EncodeJson (Maybe a)
instance encodeJsonString :: EncodeJson String
Since purescript-argonaut-codecs
provides functions to generically derive instances of EncodeJson
and DecodeJson
(gEncodeJson
and gDecodeJson
), we just need to derive a Property
instance for Generic
.
derive instance gProperty :: Generic Property
instance encodeProperty :: EncodeJson Property where encodeJson = gEncodeJson
instance decodeProperty :: DecodeJson Property where decodeJson = gDecodeJson
So now when we want to convert the data, we can call encodeJson board
, encodeJson piece
, or encodeJson property
to convert those data types into a Json
type. Then we can convert the Json
type into a string using stringify
.