Decoding JSON with Typescript
Tags: typescript, json, decoder, elm
Published: 2018-12-23 17:41:54

---
title: Decoding JSON with Typescript
published: true
description: JSON decoders validate the JSON before it comes into our program. So if the data has an unexpected structure, we learn about it immediately.
tags: typescript, json, decoder, elm
---

Typescript is very well suited for adding type safety to a JavaScript program, but on its own, it's not enough to guarantee that it won't crash at run-time.

This article shows how [JSON decoders](https://github.com/joanllenas/ts.data.json) can help to extend TypeScript compile-time guarantees to the run-time environment.


## Runtime vs Compile time
The application you are working on deals with users, so you create a `User` type:
```ts
interface User {
   firstName: string;
   lastName: string;
   picture: string;
   email: string;
}
```
You'll use this type to annotate the `/me` API endpoint result, and then you'll do all sorts of things with this `User` but let's concentrate on the profile area of the app:
- It'll display the concatenation of `firstName` + `lastName`.
- Below the `firstName` + `lastName` you also want to display the `email`.
- Finally, you want to show the `picture` of the user or, if not present, a default image.

What could go wrong? Well, for starters the `User` type is not telling the truth, It does not express all the permutations of `User` _shapes_ that the API can return.
Let's see a few examples:

```
// The result has null properties
{ firstName: "John", lastName: null, picture: null, email: "john@example.com" }

// The API returned null
null

// The result has undefined properties
{ firstName: "John", lastName: "Doe", email: "john@example.com" }

// The API contract changed and the UI team wasn't notified
{ fName: "John", lName: "Doe", picture: 'pic.jpg', email: "john@example.com" }
```

![alt text](https://thepracticaldev.s3.amazonaws.com/i/se4dj39r4rjs7qqhs986.jpg "Who cares?")

You can cope with these issues by using defensive programming techniques, a.k.a. `null` / `undefined` checks behind `if` statements but, what happens when someone else wants to use the `/me` result elsewhere? Maybe your colleague trusts the `User` type, why not? What happens then? We introduced a new vector for runtime errors.

## Enter Json decoders
You use Json decoders to make sure that a given run-time value complies with a specific compile-time type, and not only that but also gives you tools to apply transformations, failovers and more.

Json decoders have gained popularity lately thanks to [Elm](https://elm-lang.org/). 
Elm's Json decoders are a core part of the language, and they are used all over the place to ensure a smooth JS to Elm communication.

The idea behind Json decoders is that you have a collection of basic decoders ( `string`, `number`, `boolean`, `object`, `array`... ) that you can compose into more complex decoders.

## State-of-the-art JSON decoder libraries
There are a few JSON decoding libraries out there, but there's one that stood out from the rest when I made the research a while ago. [Daniel Van Den Eijkel](https://github.com/aische) created something that kept the principles of the Elm decoding library while being idiomatic in TypeScript terms.

Unfortunately, the library was unmaintained and unpublished, so I decided to fork it, polish it, and release it as an npm package under the name [ts.data.json](https://github.com/joanllenas/ts.data.json).
My contribution to the library has been documentation, better error reporting, unit testing, API improvements, a few new decoders and publishing the npm package.

## Using JSON decoders

Install the library:
```
npm install ts.data.json --save
```

### Decoding basics
Before implementing our custom `User` decoder let's try decoding a `string` from start to finish.

```ts
import { JsonDecoder } from 'ts.data.json';

console.log( JsonDecoder.string.decode('Hi!') ); // Ok({value: 'Hi!'})
```

Finished! 🎉

### Unwrapping decoder results
As we saw in our previous example the decoding process has two steps. 
- First, we declare the decoder with `JsonDecoder.string`.
- Second, we execute the decoder passing a JavaScript value with `*.decode('Hi!')`, which returns the result wrapped in an instance of `Ok`.

Why are we wrapping the result in an `Ok` instance? because in case of failure we would wrap the result in an `Err` instance.
Let's see how the `decode()` signature looks like:
```ts
decode(json: any): Result<a>
```

`Result<a>` is a union type of `Ok` and `Err`.
```ts
type Result<a> = Ok<a> | Err;
```

So most of the time we won't be using `decode()`, instead we'll probably want to use `decodePromise()`.
Let's see how the `decodePromise()` signature looks like:
```ts
decodePromise<b>(json: any): Promise<a>
```

Let's try decoding a `string` from start to finish using `decodePromise()`:

```ts
import { JsonDecoder } from 'ts.data.json';

const json = Math.random() > 0.5 ? 'Hi!' : null;
JsonDecoder.string.decodePromise(json)
  .then(value => {
    console.log(value);
  })
  .catch(error => {
    console.log(error);
  });
```

Half of the time we'll go through the `then()` route and get `Hi!`, and half of the time we'll go through the `catch()` route get `null is not a valid string`.

Now that we know the basics let's get serious and build our custom `User` decoder.

### The `User` decoder

Aside from the primitive decoders:
- `JsonDecoder.string: Decoder<string>`
- `JsonDecoder.number: Decoder<number>`
- `JsonDecoder.boolean: Decoder<boolean>`

there are also other more complex decoders, and for our `User` we'll be using the `JsonDecoder.object` decoder:
- `JsonDecoder.object<a>(decoders: DecoderObject<a>, decoderName: string): Decoder<a>`

> #### What's that `Decoder<a>` thing all decoders are returning?
> Decoders have the logic to decode a particular value, but they don't know how to execute it, this is what the `Decoder` class is for.
> `Decoder<a>` has methods to execute, unwrap, chain and transform decoders / decoder values.

Let's try decoding a `User` from start to finish using all the tricks we've learned so far:

```ts
import { JsonDecoder } from 'ts.data.json';

interface User {
  firstName: string;
  lastName: string;
}

const userDecoder = JsonDecoder.object<User>(
  {
    firstName: JsonDecoder.string,
    lastName: JsonDecoder.string
  },
  'User'
);

const validUser = {
  firstName: 'Nils',
  lastName: 'Frahm'
};

const invalidUser = {
  firstName: null,
  lastName: 'Wagner'
};

const json = Math.random() > 0.5 ? validUser : invalidUser;

userDecoder
  .decodePromise(json)
  .then(value => {
    console.log(value);
  })
  .catch(error => {
    console.log(error);
  });
```

Half of the time we'll get `{firstName: "Nils", lastName: "Frahm"}` and half of the time we'll get `<User> decoder failed at key "firstName" with error: null is not a valid string`. `JsonDecoder` has us covered.

## Going down the rabbit hole
We just started to scratch the surface of what this library is capable of, there are decoders for every type you could imagine. You can also decode:
- arrays
- dictionaries
- recursive data structures
- null
- undefined

and other fancy stuff.

Go to the [GitHub repo](https://github.com/joanllenas/ts.data.json) and find out!