A configurable JSON parser in Elm


#1

Package & docs

Check out allenap/elm-json-decode-broken. It uses elm/parser to construct the parser. I put “broken” in the name to make you think twice before using it, but also because it can parse “broken” JSON.

Parsing invalid JSON

On the face of it I built something pointless: a JSON parser in Elm. After all, Json.Decode in elm/json makes use of JSON.parse, which is native code, battle tested, fast. But it won’t parse broken JSON. In my particular case, that was JSON with strings containing carriage returns and new lines. These must be escaped in proper JSON, and JSON.parse and Json.Decode.decodeString rejected the data I needed to work with.

The right thing to do was talk to the people who were generating the invalid JSON and ask them to fix it, and I am in touch. In the meantime I use this parser to work with their data. I used the situation as an excuse to learn how to use elm/parser which was arguably more interesting than working on the end product.

An example

Below is how I used it for my particular situation. The parseAndDecode function is almost a drop-in replacement for Json.Decode.decodeString (the error types are different). You can see the basic mechanism for creating a custom parser.

import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Broken as Broken
import Json.Encode as Encode
import Parser exposing ((|.), (|=), Parser)


relaxedStringUnescaped : Parser String
relaxedStringUnescaped =
    Parser.oneOf
        [ Broken.unescaped
        , Parser.symbol "\r" |> Broken.yields "\r"
        , Parser.symbol "\n" |> Broken.yields "\n"
        ]


relaxedStringLiteral : Parser String
relaxedStringLiteral =
    Broken.stringLiteral relaxedStringUnescaped Broken.escape


relaxedString : Parser Encode.Value
relaxedString =
    Parser.map Encode.string relaxedStringLiteral


relaxedStringConfig : Broken.Config
relaxedStringConfig =
    case Broken.defaultConfig of
        Broken.Config c ->
            Broken.Config { c | string = relaxedString }


parseAndDecode : Decoder a -> String -> Result String a
parseAndDecode decoder json =
    case Broken.parseWith relaxedStringConfig json of
        Ok value ->
            Decode.decodeValue decoder value
                |> Result.mapError Decode.errorToString

        Err error ->
            Err <| Parser.deadEndsToString error