Skip to content

rowtype-yoga/purescript-yoga-json

Repository files navigation

purescript-yoga-json

yoga-json is a light-weight and simple json library for purescript.

Note: This library was initially forked from the amazing simple-json (MIT Licence). Replace your imports from Simple.JSON to Yoga.JSON to migrate.

Table of Contents

Features

  • 😌 simple and easy to use json codecs
  • 🪶 light-weight
  • 🤖 built-in support for many common types (Sum types, Tuples, BigInts, Maps, JSDate, DateTime, Eithers, NonEmptyStrings )
  • 🖍 human-friendly error reporting

Installation

spago install yoga-json

Usage

purescript-yoga-json basically provides two functions writeJSON and readJSON.

Use writeJSON to serialise a type to a JSON string:

import Yoga.JSON as JSON

serialised :: String
serialised =
  JSON.writeJSON { first_name: "Lola", last_name: "Flores" }
  -- {"last_name":"Flores","first_name":"Lola"}

Use readJSON to deserialise a JSON string:

import Yoga.JSON as JSON

deserialised :: Either MultipleErrors { first_name :: String, last_name :: String } 
deserialised = JSON.readJSON """{ "first_name": "Lola", "last_name": "Flores" }"""
-- Right { first_name: "Lola", last_name: "Flores" }

As the parsing can fail, readJSON returns an Either, potentially containing a Left data constructor with MultipleErrors. If you don't care about the specific errors, you can use readJSON_, which returns a Maybe:

deserialised :: Maybe { first_name :: String, last_name :: String } 
deserialised = JSON.readJSON_ """{ "first_name": "Lola", "last_name": "Flores" }"""
-- Just { first_name: "Lola", last_name: "Flores" }

Sum types

purescript-yoga-json provides utility functions to easily generate serialisers and deserialisers for your sum types.

Let's start with a simple example of a sum type where our data constructors do not contain any further values:

import Data.Either (Either)
import Data.Generic.Rep (class Generic)
import Yoga.JSON as JSON
import Yoga.JSON.Generics (genericReadForeignEnum, genericWriteForeignEnum)
import Yoga.JSON.Generics.EnumSumRep as Enum

data MyEnum = Enum1 | Enum2 | Enum3

Now, we need to derive a Generic instance for it:

derive instance Generic MyEnum _
-- and optionally a Show instance
instance Show MyEnum where
  show = genericShow

Next, we define a WriteForeign instance and implement the writeImpl function using genericWriteForeignEnum Enum.defaultOptions.

instance WriteForeign MyEnum
  where
  writeImpl = genericWriteForeignEnum Enum.defaultOptions

Similarly, we implement the ReadForeign instance using genericReadForeignEnum Enum.defaultOptions

instance ReadForeign MyEnum
  where
  readImpl = genericReadForeignEnum Enum.defaultOptions

That's all, we can now serialise our data type:

serialised = writeJSON { "myEnum": Enum3 }
-- {"myEnum":"Enum3"}

and deserialise it:

deserialised :: E { "myEnum" :: MyEnum }
deserialised = readJSON serialised
-- Right { myEnum: Enum3 }

Writing custom codecs

In order to write your own, custom codecs you will need to provide instances for WriteForeign and ReadForeign.

Let's see an example. We define a simple data type TrafficLight that has three data constructors Red, Yellow and Green:

data TrafficLight = Red | Yellow | Green

We would like to serialise these constructors as lower case strings "red", "yellow" and "green".

First we need to provide an implementation for the WriteForeign type class, that tells yoga-json how to serialise the type. We pattern match on the three different data constructors and write them as the three Strings red, yellow and green, using the same writeImpl function:

instance WriteForeign TrafficLight where
  writeImpl Red = writeImpl "red"
  writeImpl Yellow = writeImpl "yellow"
  writeImpl Green = writeImpl "green"

This works, because yoga-json already knows how to serialise a String.

Similarly, we need to provide an implementation for the ReadForeign type class, that tells yoga-json how to deserialise the type. We start by deserialising into a primitive type, typically a String or an Object, and then convert it to our desired Purescript type.

Since we don't know the input String, it might contain invalid data and therefore deserialisation might fail. yoga-json uses the ExceptT monad to reflect this. We can return valid values using pure and fail on invalid values:

instance ReadForeign TrafficLight where
  readImpl json = do
    -- deserialise into a string
    str :: String <- readImpl json  
    -- now we pattern match on our valid types
    case str of
      "red" -> pure Red
      "yellow" -> pure Yellow
      "green" -> pure Green
      -- and fail if we get an invalid type
      other -> fail $ ForeignError $ "Failed to parse " <> other <> " as TrafficLight"

Now we can serialise our data type:

serialised = writeJSON { "trafficLight": Red }
-- {"trafficLight":"red"}

and deserialise it:

deserialised :: E { "trafficLight" :: TrafficLight }
deserialised = readJSON """{ "trafficLight": "green" }"""
-- Right { trafficLight: Green }

deserialisedUnknown :: E { "trafficLight" :: TrafficLight }
deserialisedUnknown = readJSON """{ "trafficLight": "purple" }"""
-- Left (NonEmptyList (NonEmpty (ErrorAtProperty "trafficLight" (ForeignError "Failed to parse purple as TrafficLight")) Nil))