🎁

12 Days of Baddass Courses sale! Get instant access to the entire egghead library of courses and lessons for 58% off.

Runs out in:
13 : 08 : 55 : 40
1×
Become a member
to unlock all features
Autoplay

    Automatically decode JSON into records in ReasonML using Decco

    Murphy RandleMurphy Randle
    reasonReason

    Although ReasonML has built-in libraries for working with JSON, they are low-level and difficult to use by hand. The Decco library offers automatically generated JSON decoders, and produce a result that keeps the programmer safe at runtime by telling them whether the data they got matched the shape of the data they expected.

    Code

    Code

    Become a Member to view code

    You must be a Member to view code

    Access all courses and lessons, track your progress, gain confidence and expertise.

    Become a Member
    and unlock code for this lesson
    Discuss

    Discuss

    Transcript

    Transcript

    Instructor: Here we've got a nice VC code that fetches data from the Star Wars API and prints it out to the console. Let's see what it looks like when we run it. I'm going to run NodeSource/demo.bs.js which is the compiled JavaScript version of this recent file. As you can see, we get some JSON out. It's Luke Skywalker.

    Let's say we want to print out his name. Over here, if I type JSON.name, you'll see that I get a compiler error. It says there's an undone record field name. That's because JSON isn't a JavaScript object as far as the type system is concerned, rather it's a JSON type.

    We can't actually get field off a JSON type, unless we decode that JSON type first. That's designed to keep us safe at runtime so that we don't have problems where our type system thinks that we can safely access a field, but that field isn't there at runtime.

    Now, how can we turn JSON into something that we can get fields off of? First, we have to have a type that we turn the JSON into. Here, I'll write out type person equals add a name field and call a type string.

    Now, what's the process we use to get JSON and turn it into a person? We're going to use a language extension to do that. First, I'm going to remove this dot.name because it's breaking the compiler.

    The language extension we're going to use is called decco. Let's npm install it. npm install -- save decco. Once that's installed, we can open up our bsconfig.json. There are two things we have to do here. The first thing is going to bs dependencies and add decco.

    The next thing we have to do is find the PPX flags key. This may or not exist for you depending on whether you've already used other PPXs. I have in this project, so all I have to do is add a string and type in decco/ppx. These strings are past the executable so that the compiler can call out to extend its functionality.

    You can see that both of these PPXs use the same pattern for where they put their executable. That will not always be the same for every PPX. Refer to the README of the PPX you're using to see what the right path is. Now, I'm going to save the file and close it.

    At this point, my editor doesn't know about the changes that I made to my bsconfig, so I'm going to reload the editor. We're set to do this decoder. We do it pretty simply here by adding a square bracket right on top of our type, with the @ symbol and then the name decco.

    The language has built-in syntax for decorators, and the compiler knows that it should look to the PPX that we just added when it sees add decco. What actually happens here? If you're using Visual Studio Code, you can do a fancy little thing and show the PPX source. That means it's going to show you the source of this file after the compiler has run the PPXs.

    Here you can see that I have a type, person, with a name, string. Then right below, there are some functions that we didn't write by hand. There's a personEncode function, that takes a person and turns them into JSON, and a personDecode function, which takes JSON and does a bunch of fancy stuff to turn that JSON into a record.

    You can see that there's a bunch of code here that will make sure that the fields that you expect do exist. Otherwise, you'll get back an error result instead of a success result. Let's actually run it now and see what happens.

    Let Luke = personDecode json. Now, should I be able to do Luke.Name? Not quite, because what we have here is a result of personDecode error. That means that it could either be successful and a be person, or we could fail.

    What we can do is add a switch expression here. We're going to switch on Luke, which we should rename now to Result, because it's not Luke yet. In the case of an OK Result, we have Luke. That's where we can say, js.log to, here is the name, and then we can do, Luke.Name.

    Let's remove this pattern that got stuck in by my template automatically. There's the OK branch of our switch. Now let's add in the error branch, ErrorMessage. In this case, there's going to be a message that tells us what went wrong.

    We can see there's a path, a message, and a value, all three of those things. If it goes wrong, let's just log, "Our decoding failed," and then we'll put the whole message in there. Save the file. Our compiler's happy, so let's run it and see what it looks like.

    Here's the name, Luke Skywalker. We now successfully have a record that we can access at runtime, unpacked from the JSON that we dynamically requested. Now, what about the error case? What happens if there isn't a field that we expected.

    Let's pretend that we were looking for something like first name, instead of name. Down here, we would say, Luke.firstName instead. This is going to break, because our runtime data doesn't have any first name field, but our type system expects it. Let's run it and see what happens.

    Here you can see that the generated code has very helpfully told us that we expected a first name field, but it didn't succeed because that wasn't a string. The value, in fact, was null.

    In practice, you can use decco to save you a lot of time writing decoders by hand, and also to increase your runtime safety, by preventing you from having bugs that arise from having your types mismatch your runtime.