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.
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
:
["http://www.example.com?param=value","http://www.example2.com?param=value"]
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
}