Looks like Steve found cyan dye.

Fleecing JSON

Where we extract that juicy functionally pure data out of messy JSON strings

Michel Belleville
Published in
5 min readJul 5, 2019

--

me> Wat, I have a problem…

wat> Do you ever? 😏 What’s the matter?

me> I have this JSON file with data that I want to read…

wat> This sounds like a job for the Json.Decode library!🦸And more specifically Json.Decode.decodeString:

decodeString : Decoder a -> String -> Result Error a

me> Ok… so, we need:
- a Decoder a (whatever that is? 😐)
- a String, which I can safely assume is our JSON string input
- and I get a Result Error a which probably means the decoding can fail with an Error (which I guess is a decode-specific type that tells us what failed?) and an a (the very same a the mysterious Decoder a is working with).

wat> You’re getting good at reading types 😃 now, what do you think a Decoder a is?

me> A… thing that knows how to make an a decoding a JSON string? 😅

wat> Exactly! You can see it as a wrapper for a decoding function, a blueprint of sorts that decodeString will use to attempt to make sense of the string input.

me> Sure… now how do I get one that works for my JSON string?

wat> Most of the Json.Decode module’s functions are decoders for basic types ; here are a few examples:

string : Decoder String -- here’s one that decodes a JSON to extract a string
bool : Decoder Bool -- here’s one that searches for a boolean
int : Decoder Int -- this one will decode a JSON number to an Int
float : Decoder Float -- and this one to a Float
> decodeString string "\"nice little json string\""
Ok "nice little json string"
> decodeString bool "true"
Ok True
> decodeString int "123"
Ok 123
> decodeString float "456.789"
Ok 456.789

me> Ok, but these are for primitive types, and my JSON file is quite a bit more complicated than that… 😓

wat> Of course it is. That’s why we also have functions that will help you compose decoders so that you can decode complicated structures. Say your JSON string looks like this…

{
"id": 1234,
"name": "Ada Lovelace",
"fancies": [
"general-purpose computers",
"programming",
"mathematics"
]
}

wat> …and we want to decode it into this Elm data type…

type alias Person =
{ id: Int
, name: String
, fancies: List String
}

me> That sounds more complicated indeed.

wat> A bit ; let’s break it down 😃 we already know how to get an Int and a String from their respective JSON string representation, now let’s see how to get a List String

list : Decoder a -> Decoder (List a)

me> Ok, so, if I feed this list function a Decoder for any type a, it gives me a Decoder that decodes for a List of a?

wat> That it does 😃 it will search for a JSON list and attempt to decode all the items into whatever the Decoder a you gave it decodes. Let’s try it:

> decodeString (list string) "[\"Elm\", \"is\", \"Awesome\", \"!\"]"
["Elm", "is", "Awesome", "!"]
> decodeString (list int) "[1, 2, 3]"
[1, 2, 3]
> decodeString (list bool) "[true, false, true, false, false]"
[True, False, True, False, False]

me> Fine. But that does not tell me how to get data from an object’s fields…

wat> You have the field function for that…

field : String -> Decoder a -> Decoder a

me> Wait a minute… you give it a String, I guess it’s the name of the field… then a Decoder a… but you still get a Decoder a? How does it make sense?

wat> You can see it as a way to “zoom in” on a detail inside your JSON structure. Let me show you:

-- say we have the same person JSON data we saw before
> personJsonString = "{ \"id\": 1234, \"name\": \"Ada Lovelace\", \"fancies\": [\"general-purpose computers\", \"programming\", \"mathematics\"]}"
> decodeString (field "name" string) personJsonString
"Ada Lovelace"
> decodeString (field "id" int) personJsonString
1234
> decodeString (field "fancies" (list string)) personJsonString
["general-purpose computers", "programming", "mathematics"]

me> Ok… now… how do I put this all together?

wat> Well, your object has 3 fields, so we’ll have to use map3:

map3 :
(a -> b -> c -> value)
-> Decoder a
-> Decoder b
-> Decoder c
-> Decoder value

me> So… I feed it:

  • a function that takes the parts I need to make the whole value I eventually need (a -> b -> c -> value)
  • the decoders that give me each of those parts (Decoder a, Decoder b and Decoder c)
  • and it gives me a Decoder that produces the value I want?

wat> Yep. Putting it all together…

personDecoder : Decoder Person
personDecoder =
map3
(\id name fancies ->
{ id = id, name = name, fancies = fancies }
)
(field "id" int)
(field "name" string)
(field "fancies" (list string))
> decodeString personDecoder personJsonString
{ id: 1234
, name: "Ada Lovelace"
, fancies:
[ "general-purpose computers"
, "programming"
, "mathematics"
]
}

me> Wait a minute… this map3 looks a little bit like another map I know

wat> That’s because they’re related ; we’ll talk a little bit about how exactly later if you will, but for now let’s consider those for Json.Decode specifically

map :
(a -> r)
-> Decoder a
-> Decoder r
map2 :
(a -> b -> r)
-> Decoder a
-> Decoder b
-> Decoder r
map3 :
(a -> b -> c -> r)
-> Decoder a
-> Decoder b
-> Decoder c
-> Decoder r
map4 :
(a -> b -> c -> d -> r)
-> Decoder a
-> Decoder b
-> Decoder c
-> Decoder d
-> Decoder r
...

me> Ok, so you can use those mapN functions to get all the results from different decoders, make a new result and have it wrapped as a new Decoder that will decode everything? Neat. But I see there’s nothing higher than map8… what if I need to decode 9 fields 😈

wat> Well, there’s a new funny word we’ll talk about soon…

me> Ok. Getting tired anyways 😪 Buuuut just a last question before that. Why is the decoding function called decodeString? Why not just decode? 🤔

wat> That’s because you can use your decoders not only to decode a JSON string, but also to decode a JSON Value with decodeValue.

me> What’s a Value?

wat> It’s an opaque type (meaning Elm’s creators didn’t, who wrote the JSON module, have not provided you access to the Value type’s constructor, giving you functions to manipulate them instead, so that you don’t start poking around the type’s innards) ; you can see it as a wrapper around a functionally impure JavaScript object. Since it contains a JavaScript value, its decoding takes a lot less time and it’s a pretty useful way to move data from and to the JavaScript your Elm app will interface with, ultimately, when booting your elm app (see flags) or using ports.

me> Oh. I think I get it. Value is some wibbly-wobbly, elmy-javascripty type to pass data from and to the outer JavaScript.

wat> …good enough. I’ll be regenerating in my TARDIS (Tower Admirably Rounded for Ducky Immersion in Sleep) 💤

--

--