Search code examples
jsonelm

Elm JSON Decoder for non-string JSON Array


I'm in the process of learning Elm and have run into a wall with JSON decoders. I am building an Elm frontend that I want to feed URLs from a backend API, which returns the following JSON array of URL strings:

["http://www.example.com?param=value","http://www.example2.com?param=value"]

If I try to parse this with a List String decoder in Elm, it fails because the above is not quoted or escaped as a string. My example decoder:

stringListDecoder : Json.Decode.Decoder (List String)
stringListDecoder = Json.Decode.list Json.Decode.string

myArrDec2 = D.list D.string
D.decodeString myArrDec2 ["http://www.example.com?param=value","http://www.example2.com?param=value"]

This fails because the JSON array is seen in Elm as a string list, as opposed to a flat string that the Json.Decode.decodeString function accepts. If I were to quote and escape the array to work with this decoder, it would be in this format:

"[\"http://www.example.com?param=value\",\"http://www.example2.com?param=value\"]"

How can I write an Elm decoder that can parse the unquote/unescaped JSON array in the format being returned by my API?


UPDATE I think I failed to convey my question well enough; for that I apologize. The API is returning this value (which is valid JSON):

["http://www.example.com?param=value","http://www.example2.com?param=value"]

I'm grabbing this JSON with an Http.get and trying to run the decoder on the result, which fails because it is not a string.

-- HTTP
getUrls : Cmd Msg
getUrls =
Http.get
 { url = "http://127.0.0.1:3000/request" -- This is the endpoint returning ["http://www.example.com?param=value","http://www.example2.com?param=value"]
 , expect = Http.expectJson GotUrls urlDecoder
 }


urlDecoder : Decoder (List String)
urlDecoder =
 Json.Decode.list Json.Decode.string

Since Elm cannot accept this JSON without escaping it as a string, could I use the Http.get call to convert it to one prior to parsing with my decoder? Or am I just missing something obvious due to my inexperience with Elm? Please let me know if I can clarify further and thank you for trying to help!


FINAL EDIT It turns out after using Robert's excellent example that my own was failing due to an unrelated issue. Being new to Elm's HTTP package I was unaware of the Http.NetworkError msg, which Robert's Http error function handles. This led me to this question and ultimately revealed a CORS misconfiguration in the API.


Solution

  • Bare JSON cannot exist within Elm modules. When writing example JSON to be decoded in Elm, it must be wrapped in some other representation.

    However, that representation is specific to Elm; it is not what the decoder expects to receive from the API.

    Your decoder should work when the backend sends the literal JSON ["a","b"].


    In code:

    module Example exposing (..)
    
    import Json.Decode as JD
    
    
    stringListDecoder : JD.Decoder (List String)
    stringListDecoder =
        JD.list JD.string
    
    
    {-| Bare JSON cannot exist inside Elm syntax; it must be wrapped in something
    else. In this case, in a string.
    -}
    jsonFromApi : String
    jsonFromApi =
        "[ \"http://www.example.com?param=value\", \"http://www.example2.com?param=value\" ]"
    
    
    decoded : Result JD.Error (List String)
    decoded =
        JD.decodeString stringListDecoder jsonFromApi
    
    
    expectedResult : Result JD.Error (List String)
    expectedResult =
        Result.Ok [ "http://www.example.com?param=value", "http://www.example2.com?param=value" ]
    
    
    decodesAsExpected : Bool
    decodesAsExpected =
        decoded == expectedResult
    

    EDIT: Here's an example suitable to run with elm reactor:

    src/request.json

    ["http://www.example.com?param=value","http://www.example2.com?param=value"]
    

    src/Main.elm

    module Main exposing (..)
    
    import Browser
    import Html exposing (Html, code, div, p, pre, span, text)
    import Http
    import Json.Decode as JD
    
    
    type alias Model =
        { data : RemoteData Http.Error (List String) }
    
    
    type RemoteData err a
        = NotAsked
        | Loading
        | Loaded a
        | Failed err
    
    
    type Msg
        = GotUrls (Result Http.Error (List String))
    
    
    initialModel =
        { data = NotAsked }
    
    
    getUrls : Cmd Msg
    getUrls =
        Http.get
            { url = "/src/request.json"
    
            -- This is the endpoint returning ["http://www.example.com?param=value","http://www.example2.com?param=value"]
            , expect = Http.expectJson GotUrls urlDecoder
            }
    
    
    urlDecoder : JD.Decoder (List String)
    urlDecoder =
        JD.list JD.string
    
    
    update : Msg -> Model -> ( Model, Cmd Msg )
    update msg model =
        case msg of
            GotUrls result ->
                case result of
                    Ok strings ->
                        ( { model | data = Loaded strings }, Cmd.none )
    
                    Err error ->
                        ( { model | data = Failed error }, Cmd.none )
    
    
    view : Model -> Html Msg
    view model =
        div []
            (case model.data of
                NotAsked ->
                    [ text "no data yet" ]
    
                Loading ->
                    [ text "loading" ]
    
                Failed err ->
                    [ text "Failed... ", show err ]
    
                Loaded strings ->
                    strings |> List.map (\s -> p [] [ text s ])
            )
    
    
    show : Http.Error -> Html Msg
    show error =
        case error of
            Http.BadUrl string ->
                span [] [ text "Bad Url: ", text string ]
    
            Http.Timeout ->
                text "Timeout"
    
            Http.NetworkError ->
                text "Network error"
    
            Http.BadStatus int ->
                span [] [ text "Bad Status: ", int |> String.fromInt |> text ]
    
            Http.BadBody string ->
                span [] [ text "Bad Body: ", pre [] [ code [] [ text string ] ] ]
    
    
    main : Program () Model Msg
    main =
        Browser.element
            { init = always ( initialModel, getUrls )
            , view = view
            , update = update
            , subscriptions = always Sub.none
            }
    

    running in elm-reactor