DEV Community

Cover image for Converting JSON with Elm
José Muñoz
José Muñoz

Posted on

Converting JSON with Elm

Photo by TimJ on Unsplash

I've been getting into Elm in the last few weeks. I feel like I've been getting too comfortable with React and I wanted to get into something that was more Functional

So far I have been enjoying my experience, the concepts that the language encourages are fascinating to me, a lot of them I was already using in Javascript and it made my code better in a way that was hard to explain to my peers.

Today I want to tackle a simple task, something that literally every web application does to a certain degree, I want to call an API and present some of the data fetched. I'm using Random User API which is a free service that is consumed as REST. The final product can be found here and the source is linked below:

GitHub logo jdmg94 / elm-user-card-app

A simple elm application that fetches user data and presents a social card.

elm-user-card-app

A simple elm application that fetches user data and presents a social card. this is an exercise on parsing and transforming JSON structures we get from services that we may not have control over. it uses tailwind as its css framework, you can generate styles via yarn styles you can open a hot-reloading dev server via yarn dev it takes the same options as elm make.




Disclaimer:
I am still learning Elm, I don't think I'm qualified to teach anyone about it, however these are my thoughts and experience with it.

I won't be able to take advantage of all the data that this nice API provides and make some complicated profile builder, I would like to just show a card with a name, a picture, a handle maybe. But the response we get is rather extense:

{
  "results": [
    {
      "gender": "female",
      "name": {
        "title": "Mrs",
        "first": "Lucy",
        "last": "Cruz"
      },
      "location": {
        "street": {
          "number": 5454,
          "name": "Fincher Rd"
        },
        "city": "Bundaberg",
        "state": "New South Wales",
        "country": "Australia",
        "postcode": 6462,
        "coordinates": {
          "latitude": "-87.1460",
          "longitude": "149.9745"
        },
        "timezone": {
          "offset": "+1:00",
          "description": "Brussels, Copenhagen, Madrid, Paris"
        }
      },
      "email": "lucy.cruz@example.com",
      "login": {
        "uuid": "92878432-b5ee-4539-92ae-b3bc44880832",
        "username": "whitepanda288",
        "password": "padres",
        "salt": "t34unJhk",
        "md5": "6bbd926216c8ba43e619b6a33c3b0fa0",
        "sha1": "f2beb504929ef81d8846d8007c2ef72f5ff8f574",
        "sha256": "d4671f10b485c7e8531d70d726e1e07be4a68eb4b75c2589262abb3f36d6aefe"
      },
      "dob": {
        "date": "1983-02-22T00:20:05.732Z",
        "age": 36
      },
      "registered": {
        "date": "2015-08-05T13:35:58.976Z",
        "age": 4
      },
      "phone": "02-5505-2105",
      "cell": "0430-474-187",
      "id": {
        "name": "TFN",
        "value": "251930914"
      },
      "picture": {
        "large": "https://randomuser.me/api/portraits/women/29.jpg",
        "medium": "https://randomuser.me/api/portraits/med/women/29.jpg",
        "thumbnail": "https://randomuser.me/api/portraits/thumb/women/29.jpg"
      },
      "nat": "AU"
    }
  ],
  "info": {
    "seed": "001e6b8cb1b5ecfa",
    "results": 1,
    "page": 1,
    "version": "1.3"
  }
}

Enter fullscreen mode Exit fullscreen mode

In order to get what we need out of this large response object we need to understand decoders and type aliases. Decoders are Elm's way of handling JSON data, they provide instructions on how to handle a JSON input. Type Aliases are a schema of the data we intend on getting out of this bunch.

note:
I'll leave the boilerplate aside for all examples, however all examples should be interpreted as living in the same file.

first, let's define the data we want to receive on the view, the ideal scenario from the perspective of a view function.

type alias User = { firstName : String, lastName : String, username: String,  picture : String }
Enter fullscreen mode Exit fullscreen mode

now that we have our type alias, we can define a model, very simple:

type Model = Loading | Fail | (Success User)
Enter fullscreen mode Exit fullscreen mode

Notice that Success is encapsulated with our User type alias, elm supports the concept of tuples that allows us to bind 2 or 3 types as a single element, parenthesis are optional.

We'll come back to types, however, for now I want to transition to Decoders, since we are working on a hypothetical ideal scenario with our declarations above, we need to transform our JSON data. The first thing we're going to do is to pick the fields from the first result item, where the data resides. Type aliases are also functions, and they are initialized with their paths as curried parameters.
If your schema is rather complicated you may want to look into Json.Decode.Pipeline

userDecoder: Decoder User
userDecoder = map4 User
  (at ["name", "first"] string)
  (at ["name", "last"] string)
  (at ["login", "username"] string)
  (at ["picture", "large"] string)
Enter fullscreen mode Exit fullscreen mode

Notice we're declaring userDecoder twice, the first one is the function's prototype declaration which is going to tell the compiler what to expect from this function. The latter is the function's body, and in it we want to use the map function to return the 4 fields we need to initialize a User record, remember to send them in the order of declaration.

Now that we have the userDecoder ready we can start thinking about how to get there from the real world, we need to make a request to the server and then unpack the results object before we can use our userDecoder, in order to achieve that we will write another decoder to unwrap the results from our non-existent request:

resultsDecoder: Decoder User
resultsDecoder = field "results" index 0 userDecoder
Enter fullscreen mode Exit fullscreen mode

This decoder is also going to return a User record, but it is going to help us unwrap the layers we don't really need before extracting our data.
In order to accomplish this we're going to use the userDecoder we declared a moment ago, Decoders are just functions and being able to compose them like this really helps splitting tasks like this very easy.

Alright, I think we've done well so far with our decoders, before we can make our request I need another type that will allow me to update the application's state, and I want to give my users the option to refetch as well:

type Msg = anotherOne | gotUser (Result Error User)
Enter fullscreen mode Exit fullscreen mode

One of these enumerations of type Msg will serve as a loading flag, another one will handle when the task is done. Notice the latter is a touple, that as another touple as its second element. With that in place let's make our request:

getRandomProfile: Cmd Msg
getRandomProfile = Http.get {
    url = "https://randomuser.me/api",
    expect = Http.expectJson gotUser resultsDecoder
  }
Enter fullscreen mode Exit fullscreen mode

Using the get method of the Http package, we can pass a record with 2 members, the url to send the request, and what to expect from it's response, the Http package has other helpful functions for this, in our case we will call expectJson because that's what our decoders expect, and with that in place we can pull it together:

  browser.element = {
    init = init,
    view = view,
    update = update,
    subscriptions = subscriptions
    }

  init: () -> (Model, Cmd Msg)
  init _ = (Loading, getRandomProfile)

  subscriptions : Model -> Sub Msg
  subscriptions model = Sub.none

  update: Msg -> Model -> (Model, Cmd Msg)
  update msg model = 
    case msg of 
      AnotherOne -> (Loading, getRandomProfile)
      GotUser result -> 
        case result of 
          Ok profile -> (Success profile, Cmd.none)
          Err _ -> (Fail, Cmd.none)

  view: Model -> Html Msg
  view model = container [ 
    case model of
      Loading -> loader []
      Fail -> loader []
      Success profile -> card [ 
        onTheLeft [ roundedImage profile.picture ],
        detail [
          title (profile.firstName ++ " " ++ profile.lastName),
          subtitle ("@" ++ profile.username),
          refresh [onClick anotherOne]
          ]
        ]
    ]
Enter fullscreen mode Exit fullscreen mode

Conclusion

Elm has the right paradigms to succeed, but for now React's roadmap is looking brighter. The advantage that Elm has over React is that React had to steer the community towards the right patterns (i.e: functional components over class components) to make concurrency work. Elm by its functional nature is already capable of concurrency, but at this point its not taking advantage of it in the way that React is, and in the future, when React Concurrent mode is released the difference is going to be accentuated. If elm wants to compete in the frontend world concurrency is going to be fundamental to its longevity, nonetheless it is an enjoyable language with the right paradigms, I think this would be a great first language for someone getting into web development.

Top comments (1)

Collapse
 
mikolaj_kubera profile image
Mikolaj Kubera

"I think this would be a great first language for someone getting into web development."

I totally agree with this statement. I also think Elm is a wonderful lang to learn web dev AND programming the right way.