An elm-review rule to generate JSON decoders and encoders from a JSON sample
2021-11-29

elm-review is an excellent static code analysis tool made by Jeroen Engels. I hope it’s well known in the Elm community by now. Perhaps less known is the fact that in addition to analysing code, it’s also able to generate code.

This is actually a fundamental part of the design of elm-review as when you run it with the --fix flag, it will not only find issues but also offer to fix them when possible by replacing problematic code with an improved version.

It’s then only a small leap to go from replacing problematic code to replacing arbitrary code.

elm-review is a highly customisable system. It works by applying a set of rules to your code, with each rule (or technically a set of rules) defined in the form of an Elm package.

Some time ago, Dillon Kearns published a rule that replaces a string of HTML given to Debug.todo with elm/html code that would result in that HTML.

So you could write something like this:

import Html exposing (Html)
import Html.Attributes as Attr

navbarView : Html msg
navbarView =
    nav
        []
        [ Debug.todo """@html <ul class="flex"><li><a href="/">Home</a></li></ul>""" ]

Then you’d run elm-review --fix, the rule would spot the argument to Debug.todo with the special @html prefix (you wouldn’t want it to act on all Debug.todos!), and it would replace the code with this:

import Html exposing (Html)
import Html.Attributes as Attr

navbarView : Html msg
navbarView =
    nav 
        []
        [ Html.ul
            [ Attr.class "flex"
            ]
            [ Html.li []
                [ Html.a
                    [ Attr.href "/"
                    ]
                    [ Html.text "Home" ]
                ]
            ]
        ]

When I saw this, I realised that I could provide another “user interface” for my json2elm tool – an elm-review rule! The rule would replace a sample JSON string with a set of decoders and encoders (and relevant types) generated from it.

Today, I’m releasing alexkorban/elm-review-json-to-elm.

Similarly to Dillon’s rule, it looks for Debug.todo arguments that start with a specific prefix (@json in this case), and replaces the enclosing definition with a set of types/aliases, decoders and encoders.

For example, suppose you had this in a file:

import Json.Decode
import Json.Encode


person =
    Debug.todo """@json{"name": "John", "age": 30}"""

After running elm-review --fix with my alexkorban/elm-review-json-to-elm rule configured, you’ll get the following:

import Json.Decode
import Json.Encode


type alias Person =
    { age : Int
    , name : String
    }


personDecoder : Json.Decode.Decoder Person
personDecoder =
    Json.Decode.map2 Person
        (Json.Decode.field "age" Json.Decode.int)
        (Json.Decode.field "name" Json.Decode.string)


encodedPerson : Person -> Json.Encode.Value
encodedPerson person =
    Json.Encode.object
        [ ( "age", Json.Encode.int person.age )
        , ( "name", Json.Encode.string person.name )
        ]

Note that the name of the value (person) was taken to be the name of the top level type.

Dealing with imports

The rule actually looks at the relevant imports in the file and generates code appropriately. For example, if you wanted your decoders to use Json.Decode.Pipeline, just add the import (import Json.Decode.Pipeline) and the rule will generate this code instead:

type alias Person =
    { age : Int
    , name : String
    }


personDecoder : Json.Decode.Decoder Person
personDecoder =
    Json.Decode.succeed Person
        |> Json.Decode.Pipeline.required "age" Json.Decode.int
        |> Json.Decode.Pipeline.required "name" Json.Decode.string


encodedPerson : Person -> Json.Encode.Value
encodedPerson person =
    Json.Encode.object
        [ ( "age", Json.Encode.int person.age )
        , ( "name", Json.Encode.string person.name )
        ]

Similarly, if you have import Json.Decode.Extra, you’ll get applicative-style decoders using andMap from that module.

What’s more, the rule takes into account aliases and the exposing clause. For example, if your imports look like this:

import Json.Decode as Decode exposing (Decoder, field, int, string)
import Json.Encode as Encode

then the rule will generate the code accordingly:

type alias Person =
    { age : Int
    , name : String
    }


personDecoder : Decoder Person
personDecoder =
    Decode.map2 Person
        (field "age" int)
        (field "name" string)


encodedPerson : Person -> Encode.Value
encodedPerson person =
    Encode.object
        [ ( "age", Encode.int person.age )
        , ( "name", Encode.string person.name )
        ]

Future improvements

It doesn’t, however, cross check exposed symbols for clashes at present, which means that you can potentially get code that doesn’t compile (eg. if you expose string both from Json.Decode and Json.Encode).

While the online json2elm tool allows you to choose the naming style for decoders and encoders (verbs or nouns), there is no such option in the elm-review rule for now (in part because I couldn’t work out how that choice could be specified).

That’s it for now

I hope this rule can help you write JSON decoders and encoders faster - let me know on Twitter if it does!

Would you like to dive further into Elm?
📢 My book
Practical Elm
skips the basics and gets straight into the nuts-and-bolts of building non-trivial apps.
🛠 Things like building out the UI, communicating with servers, parsing JSON, structuring the application as it grows, testing, and so on.
Practical Elm