Simple JSON parsing with Reason and Reason-React

In the first article in this series I covered setting up your React app with ReasonML, and making HTTP POST requests to an API endpoint.   In this article I will cover decoding the JSON returned by the API into something we can use as a Reason data structure.

As a quick reminder, we used the following code to post an HTTP form to an API end point.  In my case it was a session creation end-point, so I passed an email address and password.


let loginUrl = "https://myapi.com/sessions.json";

let headers = HeadersInit.make {"Content-Type": "application/json"};

let body =
  BodyInit.make "{\"email\":\"user@user.com\",\"password\":\"verysecret\"}";

let doLogin () =>
  fetchWithInit loginUrl (RequestInit.make method_::Post ::body ::headers ())

One optimisation from before, I bring the Bs_Fetch module into scope so that I don’t need to prefix each function with Bs_fetch.


open Bs_fetch;

To work with JSON, I will use the bs-json package, which I add to my project using yarn:

yarn add bs-json

This needs to be added to the buckle script dependencies in bsconfig.json as follows:


"bs-dependencies": [
    "reason-react",
    "bs-fetch",
    "bs-json"
  ],

Restart your npm start command to pick up the change.

The fetchWithInit function returns a Javascript promise, so we use Reason’s Javascript interop to handle this.   We use Reason’s pipe operator (which should be familiar if you are used to unix shell pipes) which can be thought of as piping the output of one function into the input of another. Here’s the complete function:


let doLogin () =>
  fetchWithInit loginUrl (RequestInit.make method_::Post ::body ::headers ())
  |> Js.Promise.then_ Bs_fetch.Response.text
  |> Js.Promise.then_ (
       fun jsonText => {
         Js.log ("received response " ^ jsonText);
         Js.Promise.resolve (parseResponseJson (Js.Json.parseExn jsonText))
       }
     );

Because the call to the server is asynchronous we want the function to  continue to execute after the HTTP POST to the server has finished, so we need to wrap this call in some Javascript promises. The doLogin function returns a promise so that the code that calls it can handle it as an asynchronous function.

Js.Promise.then_ is a function that lets us handle chaining of promises as we do in Javascript.   The then_ function takes as an argument the function to call once the promise resolves successfully.  So in our chain we  wait for the call to return from the server, from which we extract from the body text response, and then pass into our function that  decodes that text (a JSON string) into our type.

So how do we convert the JSON string we just received?  That’s done in the parseResponseJson function, so let’s take a look at that next.  Firstly, let’s take a look at the format of the JSON I’m expecting.  It will have two fields, and id which is an integer, and an auth_token which is a string.   It may look something like this:


{  "id" : "556", "auth_token" : "sdjk23i2p32fdfew4323423423" }

The first thing I do is create a type to represent this in Reason:


type loginresponse = {
  id: int,
  auth_token: string
};

I then use the JSON parser in bs-json to decode the string I receive and convert it into the type.  Json.Decode returns a value of the desired type if successful or raises a DecodeError exception if not.


let parseResponseJson json :loginresponse =>
  Json.Decode.{
    id: field "id" int json,
    auth_token: field "auth_token" string json
  };

There are two new things there.  The first is I specify the return type of the function (:loginresponse).    The second is that rather than use open as we did to allow us to use the Bs_fetch functions without preceding them with the module name, we can instead use the module name, followed by a period “.” before an expression. Inside the expression we can use any export of the module without qualifying it with the module name.  This helps avoid collisions between functions with the same name from different modules.

Because of the typing that Reason gives us, the JSON we receive must contain all the fields we specify in our type record.  If the JSON is missing a field or the data is the wrong type, the decoder will return a  Json_decode.DecodeError exception specifying the missing field.

To make use of the doLogin function, we call it and then handle the promise it returns like this:


doLogin ()
      |> Js.Promise.then_ (
           fun response => {
             Js.log response.auth_token;
             Js.Promise.resolve ()
           }
         );

This logs out to the console the token string I received in the JSON response.

In my next article, I’ll look at combining stateful components and a router to start building the framework for a React app.

Leave a Reply

Your email address will not be published. Required fields are marked *