A joy of working with JSON using PureScript
Tags: javascript, webdev, purescript, typescript
Published: 2023-03-16 15:38:13

📹 Hate reading articles? Check out the complementary video, which covers the same content: https://youtu.be/fXCYKqcX1Bc

---

I’ve been working as a Fullstack engineer for a while, and it feels like we're shuffling JSON most of the coding time.

I want to cover how annoying it is to deal with JSON using JavaScript (and other languages) and share how joyful it is to work with JSON using PureScript.

*I’ll focus on JSON, but by the end, you should be able to guess and decide how it applies to other formats.*

## Status quo

### Problems when working with JSON using JavaScript

*Let’s start with the problems first.*

Just by looking at its name, JSON might give us the impression that JavaScript is the perfect language for it. But it’s not as lovely as it sounds – several issues exist.

---

💡 **JSON** stands for JavaScript Object Notation

---

As a reminder, the JSON syntax resembles JavaScript objects, but they are not the same and are not interchangeable.

```jsx
// I am a JS object
const jsData = { "name": "Jason", "quest": "Golden Fleece" };

// I am a JSON
const jsonData = '{ "name": "Jason", "quest": "Golden Fleece" }';
```

Parsing and encoding JSON can be tedious and error-prone. The built-in functions are difficult to work with and don't allow much customization or error handling.

```js
// Convert to JSON:
JSON.stringify(jsData);
// '{"name":"Jason","quest":"Golden Fleece"}'
```

```js
// Convert to JavaScript object:
JSON.parse(jsonData);
// {name: 'Jason', quest: 'Golden Fleece'}

const badData = '{ 42: "field"}';
JSON.parse(badData);
// Uncaught SyntaxError: Expected property name or '}' in JSON...
```

Dealing with JSON data with unexpected or missing keys is a pain.

```js
undefined undefined undefined
```

Working with nested JSON can be particularly challenging. Sometimes it’s just so much manual labor.

And here is a controversial byte. One of my main issues with JSON in JavaScript is dynamic typing. It’s difficult (if not impossible) to ensure that the parsed JSON data conforms to a specific interface or type.

*Don’t even get me started on dealing with something specific, like dates.*

All of these lead to many bugs and errors in code that deals with JSON.

*What if we use JSON Schema? It can help validate data but won’t solve all the issues.* 

*And what if we use TypeScript?*

### Problems when working with JSON using TypeScript

TypeScript adds static typing and provides many other benefits over JavaScript, but working with JSON in TypeScript can still be problematic.

We can define interfaces and types in TypeScript. Still, the support for custom data types is limited and ensuring that the parsed JSON data matches the defined interface or type can be challenging. Properly handling errors doesn’t get easier, either.

So, it is still time-consuming and error-prone, making the code less maintainable.

*Some libraries make it nicer. But let’s skip ahead to something that is next level nicer.*

## Working with JSON using PureScript

PureScript offers a more powerful and flexible set of tools for working with JSON data.

*To illustrate this, I’ll show you how I usually prototype with PureScript.*

Let’s grab a random sample JSON, for example, from [json.org/example.html](https://json.org/example.html):

```json
{"menu": {
    "header": "SVG Viewer",
    "items": [
        {"id": "Open"},
        {"id": "OpenNew", "label": "Open New"},
        null,
        {"id": "ZoomIn", "label": "Zoom In"},
        {"id": "ZoomOut", "label": "Zoom Out"},
        {"id": "OriginalView", "label": "Original View"},
        null,
        {"id": "Quality"},
        {"id": "Pause"},
        {"id": "Mute"},
        null,
        {"id": "Find", "label": "Find..."},
        {"id": "FindAgain", "label": "Find Again"},
        {"id": "Copy"},
        {"id": "CopyAgain", "label": "Copy Again"},
        {"id": "CopySVG", "label": "Copy SVG"},
        {"id": "ViewSVG", "label": "View SVG"},
        {"id": "ViewSource", "label": "View Source"},
        {"id": "SaveAs", "label": "Save As"},
        null,
        {"id": "Help"},
        {"id": "About", "label": "About Adobe CVG Viewer..."}
    ]
}}
```

Let’s paste it into the IDE, drop all the `null`s, and try assigning it to a variable.

```haskell
jsonSample =
  { "menu":
      { "header": "SVG Viewer"
      , "items":
          [ { "id": "Open" }
          , { "id": "OpenNew", "label": "Open New" }
          , { "id": "ZoomIn", "label": "Zoom In" }
          , { "id": "ZoomOut", "label": "Zoom Out" }
          , { "id": "OriginalView", "label": "Original View" }
          , { "id": "Quality" }
          , { "id": "Pause" }
          , { "id": "Mute" }
          , { "id": "Find", "label": "Find..." }
          , { "id": "FindAgain", "label": "Find Again" }
          , { "id": "Copy" }
          , { "id": "CopyAgain", "label": "Copy Again" }
          , { "id": "CopySVG", "label": "Copy SVG" }
          , { "id": "ViewSVG", "label": "View SVG" }
          , { "id": "ViewSource", "label": "View Source" }
          , { "id": "SaveAs", "label": "Save As" }
          , { "id": "Help" }
          , { "id": "About", "label": "About Adobe CVG Viewer..." }
          ]
      }
  }
```

Right away, the compiler tries to guess the type of the data, but because some of the objects have the field `"label"` and some don’t, the compiler brings this to our attention, so we need to make a decision:

- Is `label` an optional field?
- Or is `label` required, and there are problems with the data?

But if we had a simpler JSON, with only required fields, it would be able to suggest a type:

```haskell
json =
  { "menu":
      { "header": "SVG Viewer"
      , "items":
          [ { "id": "About", "label": "About Adobe CVG Viewer..." }
          ]
      }
  }
```

![ide suggestion](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uprrooqd0snk4pthkna9.png)

We can use this suggestion from the language server to get a free type:

```haskell
json
  :: { menu ::
         { header :: String
         , items ::
             Array
               { id :: String
               , label :: String
               }
         }
     }
json =
  { "menu":
      { "header": "SVG Viewer"
      , "items":
          [ { "id": "About", "label": "About Adobe CVG Viewer..." }
          ]
      }
  }
```

Next, we can refactor it – extract this type and use it for other values – to make it nicer:

```haskell
type Menu =
  { menu ::
      { header :: String
      , items ::
          Array
            { id :: String
            , label :: String
            }
      }
  }
```

Now, the type says that the `"label"` is required, so the sample data is wrong because some objects lack this field:

```haskell
jsonSample :: Menu
jsonSample =
  { "menu":
      { "header": "SVG Viewer"
      , "items":
          [ { "id": "SaveAs", "label": "Save As" }
          , { "id": "Help" }
--        ^^^^^^^^^^^^^^^^^^
--        Compiler error: Lacks required field "label"
          , { "id": "About", "label": "About Adobe CVG Viewer..." }
          ]
      }
  }
```

If we want, we can loosen this restriction by making the field optional by using the [`Maybe`](https://pursuit.purescript.org/packages/purescript-maybe/3.0.0/docs/Data.Maybe) data type:

![ide type error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/89c6qtrbv5d514mc1d12.png)

---

💡 The `Maybe` type represents optional values, like a type-safe `null`, where `Nothing` corresponds to `null` and `Just x` – the non-null value `x`.

---

We must adopt the code and make the value “presence” explicit.

```haskell
import Data.Maybe (Maybe(..))
```

```haskell
jsonSample :: Menu
jsonSample =
  { "menu":
      { "header": "SVG Viewer"
      , "items":
          [ { "id": "SaveAs", "label": Just "Save As" }
          , { "id": "Help", "label": Nothing }
          , { "id": "About", "label": Just "About Adobe CVG Viewer..." }
          ]
      }
  }
```

*This might look tedious, but it’s only because I’m trying to show you the code and walk you through the steps. Also, we’re modifying a PureScript record, not a JSON.*

---

💡 Records correspond to JavaScript's objects; record literals have the same syntax as JavaScript's object literals.

---

💡 We can drop the quotations around the field labels:

```haskell
jsonSample :: Menu
jsonSample =
  { menu:
      { header: "SVG Viewer"
      , items:
          [ { id: "SaveAs", label: Just "Save As" }
          , { id: "Help", label: Nothing }
          , { id: "About", label: Just "About Adobe CVG Viewer..." }
          ]
      }
  }
```

---

Usually, you just use the library for stuff like this. We’ll showcase one of the neat libraries in the next section.

One of the most significant benefits of working with JSON in PureScript is how easy it’s to create types that correspond directly to JSON objects, which empowers the compiler to detect any mismatches between the actual data and the expected type, making it easier to catch errors and guarantee that the code works as expected.

```haskell
jsonSampleWithTypo :: Menu
jsonSampleWithTypo =
  { menu:
      { header: "SVG Viewer"
      , items:
          [ { id: "SaveAs", label: Just "Save As" }
          , { id: "Help", label: Nothing }
          , { id: "About", labe: Just "About Adobe CVG Viewer..." }
--                         ^^^^
--                         Compiler error          
          ]
      }
  }
```

PureScript allows us to ensure that we can correctly parse the incoming JSON and validate that all the required fields are present.

### JSON libraries

PureScript also offers several libraries for working with JSON, such as [`purescript-yoga-json`](https://github.com/rowtype-yoga/purescript-yoga-json), which provides JSON encoding and decoding functions that are pretty flexible and customizable.

### JSON decoding

First, let’s start with proper decoding. Imagine we fetched a string from an external service or a database (but here, it’s just a mock):

```haskell
rawJson :: String
rawJson = """ { "id": "About", "label": "About Adobe CVG Viewer..." } """
```

---

💡 We can use `"""` to wrap multiline strings.

---

We don’t have to bother with any decoders or parsers or whatever. We just specify what we care about as a type and use a library function:

```haskell
import Yoga.JSON (readJSON_)

type MenuItem =
  { id :: String
  , label :: Maybe String
  }

item :: Maybe MenuItem
item = readJSON_ rawJson
-- (Just { id: "About", label: (Just "About Adobe CVG Viewer...") })
```

We use [`readJSON_`](https://pursuit.purescript.org/packages/purescript-yoga-json/4.0.1/docs/Yoga.JSON#v:readJSON_), which tries to parse a JSON string to a type `a`, returning `Nothing` if the parsing fails.

---

💡 Using `Maybe`s is nice and straightforward for prototyping but can be limiting in production because we throw away the actual error.

The library provides two other functions that allow different kinds of error handling; for instance, a function returns a list of all the parsing errors in case of a failure.

---

When parsing, extra fields are going to be ignored and not parsed:

```haskell
itemExtra :: Maybe MenuItem
itemExtra = readJSON_ """ { "id": "About", "label": "About Viewer", "extra": "fields are ignored" } """
-- (Just { id: "About", label: (Just "About Viewer") })
```

The decoding will work when the optional fields are missing:

```haskell
itemNoLabel :: Maybe MenuItem
itemNoLabel = readJSON_ """ { "id": "About" } """
-- (Just { id: "About", label: Nothing })
```

But it won’t work when the JSON doesn’t have the required field or is completely broken:

```haskell
itemWrong :: Maybe MenuItem
itemWrong = readJSON_ """ { "identity": "About" } """
-- Nothing 

itemBroken :: Maybe MenuItem
itemBroken = readJSON_ """ { 42: "About" } """
-- Nothing
```

*If we use an alternative function, we can see the actual errors and act on them in particular ways:*

- *The first one would return an error about missing fields.*
- *The second one would return an error about a broken JSON.*

*Which we can handle differently according to the business domain.*

---

*JSON encoding is also as simple, so let’s just skip it.*

These libraries enable developers to handle unexpected data more gracefully and customize the encoding and decoding process to fit their needs.

### Working with JSON

Because parsed JSONs are PureScript records, we don’t need a library to work with them; the standard library provides excellent tools for numerous modification tasks.

For example, let’s write a simple function that can be used to set the `header` field:

```haskell
setHeader :: String -> Menu -> Menu
setHeader newHeader config =
  config { menu { header = newHeader } }
```

We can use it with the sample JSON:

```haskell
emptyHeadedMenu = setHeader "empty" jsonSample
-- { menu: { header: "empty", items: [{ id: "SaveAs", label: (Just "Save As") },{ id: "Help", label: Nothing },{ id: "About", label: (Just "About Adobe CVG Viewer...") }] } }

emptyHeadedMenu.menu.header
-- "empty"
```

And we don’t need to clone anything, no need for getters or setters – we use standard record update syntax.

The cool thing is that we can easily make this function work for various JSONs. If we drop the specialized type signature, the function becomes generic:

```haskell
setAnyHeader newHeader config =
  config { menu { header = newHeader } }

setAnyHeader "empty" jsonSample
-- { menu: { header: "empty", items: [{ id: "SaveAs", label: (Just "Save As") },{ id: "Help", label: Nothing },{ id: "About", label: (Just "About Adobe CVG Viewer...") }] } }

setAnyHeader "empty" { menu: { header: "Head", body: "Body" } }
-- { menu: { header: "empty", body: "Body"} }
```

This is a different type of data, but the function still works.

---

💡 We can add an explicit type signature:

```haskell
setAnyHeader
  ∷ String
  → { menu ∷ { header :: String | _ } | _ }
  → { menu ∷ { header :: String | _ } | _ }
setAnyHeader newHeader config =
  config { menu { header = newHeader } }
```

Which says that the function takes and returns any record that has a field `menu` that is any record that has a text field `header`. 

*The function doesn’t care about the rest of the fields and keeps them as is.*

---

Also, the typos and invalid data won’t compile:

```haskell
setAnyHeader "empty" { menu: { head: "Head", body: "Body" } }
--                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-- Compile error: Type of expression lacks required label header.
```

Moreover, PureScript provides a standard package called [`record`](https://pursuit.purescript.org/packages/purescript-record/1.0.0/docs/Record) for working with records, including neat functions for modifying records, removing duplicate labels, renaming labels, etc.

One of my favorites is [`union`](https://pursuit.purescript.org/packages/purescript-record/1.0.0/docs/Record#v:union), which we can use to merge records, for example, if they come from different services or pipelines. Here is an example:

```haskell
menuPart = { menu: { header: "SVG Viewer", body: "Body" } }

metaPart = { metaInfo: "Additional information" }

whole = union menuPart metaPart
-- { menu: { body: "Body", header: "SVG Viewer" }, metaInfo: "Additional information" }
```

## Conclusion

Dealing with data, such as JSON, is one of the areas where using a language like PureScript can be beneficial, as it provides more robust and customizable tooling. 

You can be in control of the data and error-handling. And you can forget about running into `undefined` at runtime.

---

💡 Links, recaps, and cheat sheets:
* [Simple JSON transformation with PureScript](https://impurepics.com/posts/2019-07-21-purescript-json.html) 

* [purescript-yoga-json](https://github.com/rowtype-yoga/purescript-yoga-json)