Elm and JSON

There is a whole section in The Official Guide about Decoders. page , commit

We just saw an example that uses HTTP to get the content of a book. page , commit

Our next example shows how to fetch some JSON data. commit

We modify the example to get a simple Elm application that fetches JSON data from a specific URL – a Wiki Page JSON instead of a random quote – and displays it on a webpage. commit

We see "I could not load the Wiki Page JSON for some reason." and consult the Elm Debugger.

Wiki Page JSON View, Elm Debugger and Console Log

The Model type represents the different states of the application.

type Model = Loading | Success Wiki.Page | Failure String

It can be Loading if the data is retrieved, Success if the data, i.e. a Page was successfully retrieved, or Failure if the data retrieval fails.

The Page type represents the structure of the retrieved JSON data.

type alias Page = { title : String , story : List Story , journal : List Journal }

It contains a title field of type String, a story field containing a list of Story objects, and a journal field containing a list of Journal objects

Elm Debugger

In the Elm Debugger we see that the current state is Failure. And the current message is

GotPage Err … 0 = BadBody "Problem...`story`"

We see that our problem is related to the story, i.e. the value at json.story.

We introduce the parts of a Federated Wiki page. The "story" is a collection of paragraphs and paragraph like items. The "journal" collects story edits. Should you take my page and edit it as yours, I can see what you've done and may decide to take your edits as my own.

The code includes various JSON decoders using the Json.Decode module. These decoders are used to decode the JSON response into Elm types.

Our page decoder initially looks like this:

decodePage : Decoder Page decodePage = map3 Page (field "title" string) (field "story" (list decodeStory)) (field "journal" (list decodeStoryEdit))

decodePage decodes the JSON response into a Page object.

The story decoder is still incomplete:

decodeStory : Decoder Story decodeStory = map3 Story (field "type" string) (field "id" string) (field "text" string)

decodeStory decodes a story object.

decodeStoryEdit : Decoder Journal decodeStoryEdit = map4 Journal (field "type" string) (field "id" string) (field "item" decodeJournalItem) (field "date" int) decodeJournalItem : Decoder Item decodeJournalItem = map3 Item (field "type" string) (field "id" string) (field "text" string)

decodeStoryEdit decodes a journal object, and decodeJournalItem decodes a (story) item object within a journal.

Regarding the GotPage Err message, we added some debug code to log the JSON content to the console in case of a BadBody error. [⇒ Decoding JSON HTTP Responses, The BadBody Error]

The Http.BadBody case is used to handle the scenario where the HTTP request returns a response with a bad or unexpected body content. This case is triggered when the HTTP response body cannot be successfully decoded according to the expected JSON structure.

Here's how the Http.BadBody case is handled in the code:

Http.BadBody body -> let _ = Debug.log "GotPage JSON:" body in "Bad Body: " ++ body

In this case, the body parameter represents the content of the response body. To assist with debugging, the Debug.log function is used to log the content of the response body, printing it to the browser's developer console. This helps in inspecting the received JSON content for troubleshooting purposes. After logging the body content, the code returns a descriptive error message with the body content. This error message will be displayed in the UI when the Failure state is reached.

Note that when handling the Http.BadBody case in this way, it is assumed that the response body is a valid JSON structure, and if it is not, it is considered a "bad body".

In the console log we saw the message "Problem with the value in json.story":

Object { "GotPage JSON": "Problem with the value at json.story:\n\n [\n {\n \"type\": \"paragraph\",\n \"id\": \"c0fc5dfa9719f0b8\",\n \"text\": \"There is a whole section in The Official Guide about Decoders. page , commit \"\n },\n {\n \"type\": \"paragraph\",\n \"id\": \"322b1d9c81270cc8\",\n \"text\": \"We just saw an example that uses HTTP to get the content of a book. page , commit \"\n },\n {\n \"type\": \"paragraph\",\n \"id\": \"bfeaa7d33f192ace\",\n \"text\": \"Our next example shows how to fetch some JSON data. commit \"\n },\n […] {\n \"type\": \"paragraph\",\n \"id\": \"ec74725a8c564752\",\n \"text\": \"How can I export console log […]? stackoverflow \"\n }\n ]\n\nExpecting an OBJECT with a field named `story`" } elm-console-debug.js:2:104200

At the end we get an expectation: "Expecting an OBJECT with a field named `story`".


JSON decoding: BadBody error discourse , ellie


How can I export console log […]? stackoverflow

Is it possible to conditionally decode certain fields using elm-decode-pipeline stackoverflow

Conditional JSON decoder with optional fields discourse , ellie

Based on the provided JSON structure for the "journal" part of a wiki page, here's how the conditional Elm JSON decoders could look like:

import Json.Decode exposing (Decoder, at, string, field, maybe, int, oneOf, succeed) type alias JournalEntry = { entryType : EntryType , id : String , item : Item , date : Int } type EntryType = Edit | Add | Move type alias Item = { itemType : String , id : String , text : String } decodeJournalEntry : Decoder JournalEntry decodeJournalEntry = let decodeEdit = succeed Edit decodeAdd = succeed Add decodeMove = succeed Move decodeItem = field "item" <| Item <$> field "type" string <*> field "id" string <*> field "text" string in field "type" string |> andThen decodeEntryType |> andMap2 (field "id" string) decodeItem |> andMap2 (field "date" int) decodeEntryTypeDetails decodeEntryType : String -> Decoder EntryType decodeEntryType entryType = case entryType of "edit" -> succeed Edit "add" -> succeed Add "move" -> succeed Move _ -> fail ("unknown entry type: " ++ entryType) decodeEntryTypeDetails : EntryType -> Item -> Int -> Decoder JournalEntry decodeEntryTypeDetails entryType item date = case entryType of Edit -> succeed (JournalEntry Edit item date) Add -> andThen (field "after" (maybe string)) decodeAddDetails |> map (\after -> JournalEntry Add { item | after = after } date) Move -> field "order" (list string) |> map (\order -> JournalEntry Move { item | order = order } date) decodeAddDetails : Maybe String -> Decoder (Maybe String) decodeAddDetails maybeAfter = case maybeAfter of Just after -> succeed (Just after) Nothing -> succeed Nothing

In this example, we define the types JournalEntry, EntryType, and Item to represent the structure of the JSON data. The JournalEntry type has fields for entryType, id, item, and date. The EntryType type represents first only some of the possible entry types: Edit, Add, and Move. The Item type represents the structure of the item field.

The decodeJournalEntry function is the main decoder for the journal entries. It uses andThen and andMap2 to conditionally decode the entry type and the item details based on the entry type value. The decodeEntryType function decodes the entry type string and returns the appropriate EntryType variant. The decodeEntryTypeDetails function takes the entry type, item, and date and further decodes the entry details based on the entry type. It returns a decoder for the JournalEntry.

The decodeAddDetails function is used within decodeEntryTypeDetails to handle the optional "after" field in the Add entry type.

Note: The code assumes that you have defined the necessary types (Edit, Add, Move, Item, etc.) in your Elm module.


Over the last few days, I've been working on serializing and deserializing our wiki pages in terms of JSON data. In other words, how a page stored in a server directory is decoded by a wiki client and re-encoded (serialized) as JSON after changes. commit