Decoding Nested JSON Objects in ReasonML with bs-json

Joe Crick
ITNEXT
Published in
4 min readNov 19, 2018

--

In the Wild West of JavaScript, parsing JSON data is trivial. If we have the following JSON:

const myJson = {
"page": 1,
"pageCount": 12,
"pageSize": 25,
"items": [
{
"id": 12,
"active": true,
"title": "The Trials and Tribulations of Tristram Shandy.",
"date": {
"start": "2004-09-16T23:59:58.75",
"end": "2004-09-16T23:59:58.79"
},
"code": "12345"
},
{
"id": 10,
"active": true,
"title": "The Life and Letters of Leonard Lego.",
"date": {
"start": "2004-09-16T23:59:58.75",
"end": "2004-09-16T23:59:58.79"
}
}
]
};

The following code will parse it:

JSON.parse(myJson);

Accomplishing the same task in the strongly-typed world of ReasonML can involve quite a bit more code. In general, Reason is explicit. In this case, that means you’ll need to declare your data types and your decoders.

In this article we’ll review how to decode nested JSON, as in the example above, using one of the most common ReasonML JSON encode/decode libs bs-json.

bs-json is a compositional JSON encode/decode library for BuckleScript. It provides a set of decoder functions you can compose into more complex decoders. BuckleScript transpiles OCaml/ReasonML to JavaScript. For in-depth details on bs-json, visit their docs:(https://github.com/glennsl/bs-json).

The docs do a great job of describing how to setup bs-json, and do basic JSON parsing. What they don’t go into detail on is how to decode complex JSON objects.

If you’ve ever done JSON decoding in something like C#, or Java, decoding and encoding JSON in ReasonML will be a very familiar experience. However, if all you’ve ever done is JavaScript, JSON decoding in ReasonML could feel a little daunting. If that’s you, hopefully this article will help you out.

The Sample App

Imagine we’re dealing with an application that lists books for sale. The home page shows a list of the books on offer. We query an API, that returns us a JSON object with all of the books.

The Decoding Process

I’m going to assume that you’ve followed the steps on the bs-json site to setup your application (If not, here’s the link again: https://github.com/glennsl/bs-json).

The first step to decoding your JSON is writing a JSON decoder. Writing the decoder involves three steps:

  1. Write types to define your data (if you haven’t already)
  2. For each type, write a decoder
  3. Call the decoder

Write types

Let’s use the JSON data we defined above. Here it is again, for convenience:

const myJson = {
"page": 1,
"pageCount": 12,
"pageSize": 25,
"items": [
{
"id": 12,
"active": true,
"title": "The Trials and Tribulations of Tristram Shandy.",
"date": {
"start": "2004-09-16T23:59:58.75",
"end": "2004-09-16T23:59:58.79"
},
"code": "12345"
},
{
"id": 10,
"active": true,
"title": "The Life and Letters of Leonard Lego.",
"date": {
"start": "2004-09-16T23:59:58.75",
"end": "2004-09-16T23:59:58.79"
}
}
]
};

Looking at the data, we have at least three types. One type should define the set of items. Another type should define the individual items. The last type should define the date. In this example, we’re just defining a type for each object. In your code you may have types defined for individual properties, as well.

Below are the defined types for the above JSON. These types are not perfect. However, they are simple, and they will serve to illustrate what we need to do.

type bookDate = {
start: string,
end: string
}
type book = {
id: int,
active: bool,
title: string,
date: bookDate,
code: option(string),
};
type bookOffers = {
page: int,
pageCount: int,
pageSize: int,
itemCount: int,
items: array(book),
};

For each type, we create a decoder:

let decodeBookDate= json =>
Json.Decode.{
start: json |> field("start", string),
end: json |> field("end", string)
};
let decodeBook= json =>
Json.Decode.{
id: json |> field("id", int),
active: json |> field("active", bool),
title: json |> field("title", string),
date: json |> field("date", decodeBookDate),
code: json |> optional(field("code", string))
};
let decodeBookOffers = json =>
Json.Decode.{
page: json |> field("page", int),
pageCount: json |> field("pageCount", int),
pageSize: json |> field("pageSize", int),
itemCount: json |> field("itemCount", int),
items: json |> field("items", array(decodeBook)),
};

Notice how the decoders interact. For example, the decoder for the items property of the decodeBookOffers decoder, points to the decodeBook decoder. Too, the date property of the decodeBook decoder, points to the decodeBookDate decoder.

The next, and last thing to do, is actually use your decoder. This part is pretty straight forward:

Js.Promise.(
Fetch.fetch("http://my.api.com/bookOffers?page=1&pageSize=10")
|> then_(Fetch.Response.json)
|> then_(json =>
json |> BookDecoder.decodeBookOffers
|> (offers => send(OffersFetched(offers)))
|> resolve
)
|> catch(_err => {
Js.log(_err);
Js.Promise.resolve(send(OffersFailedToFetch));
})
|> ignore
);

And, that’s how you decode a nested object using bs-json. Enjoy!

--

--