510 lines
15 KiB
Elm
510 lines
15 KiB
Elm
module Json.Starter
|
|
exposing
|
|
( Field
|
|
, Json
|
|
, Outcome(..)
|
|
, decodeAll
|
|
, decodeObject
|
|
, errorsToString
|
|
, fromString
|
|
, nullable
|
|
, required
|
|
, toDecoder
|
|
)
|
|
|
|
{-| Decode JSON values into Elm values.
|
|
|
|
Let's say we have a `User` type, and some JSON that represents one:
|
|
|
|
type alias User =
|
|
{ name : String
|
|
, stars : Int
|
|
, email : Maybe String
|
|
, administrator : Bool
|
|
}
|
|
|
|
userJSON : String
|
|
userJSON =
|
|
"""
|
|
{
|
|
"name": "Sam Sample",
|
|
"num_stars": 5,
|
|
"email": "sam@sample.com",
|
|
"auth_token": "abc93fd3b",
|
|
"is an admin": false,
|
|
}
|
|
"""
|
|
|
|
There are a few differences between the two.
|
|
|
|
- The JSON object has an `auth_token` field that we don't care about.
|
|
- The `stars` field in the Elm record is called `num_stars` in the JSON object.
|
|
- The `administrator` field in the Elm record is called `is an admin` in JSON.
|
|
- The `email` field in the Elm record is a `Maybe String`, but JSON doesn't have `Maybe`.
|
|
|
|
We'll resolve all three of these differences in the course of decoding the JSON.
|
|
Heres the function which will do this:
|
|
|
|
decodeUser : Json -> Result String User
|
|
decodeUser json =
|
|
case
|
|
decodeObject json
|
|
[ required [ "name" ]
|
|
, nullable [ "email" ]
|
|
, required [ "is an admin" ]
|
|
, required [ "num_stars" ]
|
|
]
|
|
of
|
|
[ String name, MaybeString email, Bool administrator, Number stars _ ] ->
|
|
Ok
|
|
{ name = name
|
|
, email = email
|
|
, administrator = administrator
|
|
, stars = stars
|
|
}
|
|
|
|
errors ->
|
|
Err (Json.errorsToString errors)
|
|
|
|
This function takes a `Json` value (we'll see how to get one later) and returns
|
|
`Ok` with the resulting `User` if decoding succeeded, and `Err` with an error
|
|
message `String` if it failed. Here are some ways decoding could fail:
|
|
|
|
- The JSON was malformed
|
|
- One of the expected fields (such as `email`) was missing
|
|
- A value had an unexpected type (for example, `email` was a number)
|
|
|
|
The call to `decodeObject` specifies which fields we expect, and what their
|
|
types should be.
|
|
|
|
decodeObject json
|
|
[ required [ "name" ]
|
|
, nullable [ "email" ]
|
|
, required [ "is an admin" ]
|
|
, required [ "num_stars" ]
|
|
]
|
|
|
|
This says that we expect the `name`, `email`, `num_stars`, and `is an admin`
|
|
fields to be present in the JSON. There may be additional fields present (such
|
|
as `auth_token`) but we don't care about them.
|
|
|
|
The distinction between `required` and `nullable` is that `name`, `num_stars`,
|
|
and `is an admin` must not be `null`, but we expect that `email` might be `null`.
|
|
|
|
Next we have a _case-expression_ with a branch that specifies the types of
|
|
values we expect to find in this JSON.
|
|
|
|
[ String name, MaybeString email, Bool administrator, Number stars _] ->
|
|
|
|
The order we use for this list corresponds to the order we specified our
|
|
`nullable` and `required` fields earlier. Because we had `required "name"`
|
|
first in that list, we have `String name` first in this list, and so on.
|
|
|
|
Here's what these types do:
|
|
|
|
- `String name` confirms that `required "name"` found a string in the JSON, and assigns it to the `name` variable. That `name` variable's type is `String`.
|
|
- `MaybeString email` confirms that `nullable "email"` found either a string or a `null` in the JSON. If it found `null`, it assigns `Nothing` to the `email : Maybe String` variable, and if it found a string, it assigns `Just` that string.
|
|
- `Bool administrator` confirms that `required "is an admin"` found a boolean in the JSON, and assigns it to `administrator : Bool`.
|
|
- `Number stars _` confirms that `required "num_stars"` found a number. Since JSON does not distinguish between integers and floats, `Number` presents both alternatives; use `Number intGoesHere _` to get the integer (with any decimals truncated) and `Nuumber _ floatGoesHere` to get the float. Since we wanted an integer, we used `Number stars _`. If `stars` were a `Float`, we would have used `Number _ stars` instead.
|
|
|
|
Now that we have the decoded values in Elm variables, we can use them to return
|
|
a `User` record.
|
|
|
|
[ String name, MaybeString email, Bool administrator, Number stars _ ] ->
|
|
Ok
|
|
{ name = name
|
|
, email = email
|
|
, administrator = administrator
|
|
, stars = stars
|
|
}
|
|
|
|
If anything went wrong - for example, the JSON was malformed, or there was no
|
|
`email` field, or `stars` was a string instead of a number - then the other
|
|
branch of the _case-expression_ will get run:
|
|
|
|
errors ->
|
|
Err (errorsToString errors)
|
|
|
|
The `errorsToString` function generates an error message string. This message
|
|
is for the benefit of programmers, not users (who can't do much with a message
|
|
like "this JSON was malformed") - so this string should be used for
|
|
behind-the-scenes logging purposes, if at all.
|
|
|
|
|
|
## Nested JSON
|
|
|
|
Sometimes we need to deal with JSON that has a nested structure. For example,
|
|
what if our example JSON had `name` and `email` nested under a `user` field?
|
|
|
|
userJSON : String
|
|
userJSON =
|
|
"""
|
|
{
|
|
"user": {
|
|
"name": "Sam Sample",
|
|
"email": "sam@sample.com",
|
|
},
|
|
"num_stars": 5,
|
|
"auth_token": "abc93fd3b",
|
|
"is an admin": false,
|
|
}
|
|
"""
|
|
|
|
We can decode this by adding `"user"` before the `"name"` and `"email"` fields:
|
|
|
|
decodeObject json
|
|
[ required [ "user", "name" ]
|
|
, nullable [ "user", "email" ]
|
|
, required [ "is an admin" ]
|
|
, required [ "num_stars" ]
|
|
]
|
|
|
|
This will use `user.name` for the first value in the list and `user.email` as
|
|
the second value. We can add as many of these as we like; for example,
|
|
`[ "user", "account", "email" ]` would decode from `user.account.email`.
|
|
|
|
We can also handle nested JSON by calling `decodeObject` multiple times.
|
|
Let's say we had some JSON with a `users` field, which held a JSON array of
|
|
objects that fit the pattern of the individual "user" JSON we decoded earlier.
|
|
|
|
We could write a function which decodes this JSON to a `List User` like so:
|
|
|
|
decodeUsers : Json -> Result String (List User)
|
|
decodeUsers json =
|
|
case decodeObject json [ required [ "users" ] ] of
|
|
[ List usersJson ] ->
|
|
decodeAll usersJson decodeUser
|
|
|
|
errors ->
|
|
Err (errorsToString errors)
|
|
|
|
`decodeObject json [ required [ "users" ] ]` looks about the same as what we did
|
|
before. The error branch looks identical to the one we wrote last time. The only
|
|
difference is that it decodes to a `List` instead of a `String` or `Bool` like
|
|
before:
|
|
|
|
[ List usersJson ] ->
|
|
decodeAll usersJson decodeUser
|
|
|
|
This `usersJson` value has the type `List Json`, which means we can use a
|
|
function like `decodeUser` to translate it from JSON into an Elm type.
|
|
|
|
The `decodeAll` function does exactly what we want here: it applies our
|
|
`decodeUser` function to each of the `Json` values in `usersJson : List Json`.
|
|
If all those decoding operations succeed, we get back an `Ok` with a `List User`
|
|
inside. If any of them fail, we instead get back an `Err` with a `String` inside.
|
|
|
|
Finally, we can get `Json` values in one of two ways. One is directly from
|
|
a string, using [`fromString : String -> Json`](#fromString). Another is
|
|
by using [`toDecoder`](#toDecoder) which gives us a `Decoder` value that is
|
|
commonly used by packages like [`elm-lang/http`](http://package.elm-lang.org/packages/elm-lang/http/latest).
|
|
|
|
|
|
## Upgrading to Decoders
|
|
|
|
You now know how to turn raw JSON into validated and parsed Elm values.
|
|
Congratulations! You're now ready to get started building Elm things that
|
|
interact with JSON.
|
|
|
|
So why is this library called a Starter Kit, anyway?
|
|
|
|
The [Decoder](http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode)
|
|
library is what most production applications end up using for their JSON needs.
|
|
It's generally more flexible, composable, and concise than this Starter Kit,
|
|
but it's not ideal for getting up and running because there are several more
|
|
concepts to learn before you can start using it.
|
|
|
|
Fortunately, you don't need a JSON toolbox with all the trimmings to get up
|
|
and running, and having a nice incremental progression is great for learning!
|
|
(Not to worry - it's straightforward to upgrade to Decoders once you find
|
|
yourself craving something more than what this library gives you.)
|
|
|
|
Until then, feel free to put it out of your mind and have fun building things!
|
|
|
|
|
|
# JSON
|
|
|
|
@docs Json, fromString
|
|
|
|
|
|
# Turning JSON into Result
|
|
|
|
@docs decodeObject, errorsToString
|
|
@docs nullable, required
|
|
@docs Outcome(..)
|
|
@docs Field
|
|
|
|
|
|
# Converting decoders
|
|
|
|
@docs toDecoder, decodeAll
|
|
|
|
-}
|
|
|
|
import Json.Decode as Decode exposing (Decoder)
|
|
|
|
|
|
-- TYPES --
|
|
|
|
|
|
{-| A JSON string, or a value from JavaScript (which has the same structure as
|
|
JSON - for example, an event object).
|
|
-}
|
|
type Json
|
|
= JsonString String
|
|
| JsonValue Decode.Value
|
|
|
|
|
|
{-| The outcome from decoding a [`Json`](#Json) value.
|
|
|
|
[`decodeObject`](#decodeObject) returns one of these.
|
|
|
|
-}
|
|
type Outcome
|
|
= String String
|
|
| MaybeString (Maybe String)
|
|
| Number Int Float
|
|
| MaybeNumber (Maybe Int) (Maybe Float)
|
|
| Bool Bool
|
|
| MaybeBool (Maybe Bool)
|
|
| Object Json
|
|
| MaybeObject (Maybe Json)
|
|
| List (List Json)
|
|
| MaybeList (Maybe (List Json))
|
|
| DecodingError String
|
|
|
|
|
|
{-| A field in a [`Json`](#Json) object.
|
|
-}
|
|
type Field
|
|
= Required (List String)
|
|
| Optional (List String)
|
|
|
|
|
|
|
|
-- PUBLIC FUNCTIONS --
|
|
|
|
|
|
{-| Convert a JSON string to a [`Json`](#Json) value.
|
|
-}
|
|
fromString : String -> Json
|
|
fromString str =
|
|
JsonString str
|
|
|
|
|
|
{-| A required field in a [`Json`](#Json) object.
|
|
-}
|
|
required : List String -> Field
|
|
required =
|
|
Required
|
|
|
|
|
|
{-| A field in a [`Json`](#Json) object that can potentially
|
|
be `null`. This works like [`required`](#required), except
|
|
the result will be a [`Maybe`](http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Maybe).
|
|
-}
|
|
nullable : List String -> Field
|
|
nullable =
|
|
Optional
|
|
|
|
|
|
{-| Decode some fields from an object. If the object has more fields than the
|
|
requested ones, they will be ignored.
|
|
-}
|
|
decodeObject : Json -> List Field -> List Outcome
|
|
decodeObject json fields =
|
|
case List.foldr (decodeField json) (Ok []) fields of
|
|
Ok outcomes ->
|
|
outcomes
|
|
|
|
Err errors ->
|
|
errors
|
|
|
|
|
|
{-| Run a `(Json -> Result)` function on each `Json` value in a list. If they
|
|
all return `Ok`, then return `Ok` with a list of those values. If any of them
|
|
returns `Err`, return `Err`.
|
|
|
|
From the example in this module's introduction:
|
|
|
|
decodeUsers : Json -> Result String (List User)
|
|
decodeUsers json =
|
|
case decodeObject json [ required [ "users" ] ] of
|
|
[ List usersJson ] ->
|
|
decodeAll usersJson decodeUser
|
|
|
|
errors ->
|
|
Err (errorsToString errors)
|
|
|
|
-}
|
|
decodeAll : List Json -> (Json -> Result String a) -> Result String (List a)
|
|
decodeAll values decodeFn =
|
|
case List.foldr (decodeAllHelp decodeFn) (Ok []) values of
|
|
Ok decodedValues ->
|
|
Ok decodedValues
|
|
|
|
Err errors ->
|
|
Err (String.join "\n\n" errors)
|
|
|
|
|
|
decodeAllHelp :
|
|
(Json -> Result String a)
|
|
-> Json
|
|
-> Result (List String) (List a)
|
|
-> Result (List String) (List a)
|
|
decodeAllHelp decodeFn json result =
|
|
case ( result, decodeFn json ) of
|
|
( Ok values, Ok value ) ->
|
|
Ok (value :: values)
|
|
|
|
( (Err _) as outcomes, Ok _ ) ->
|
|
outcomes
|
|
|
|
( Ok outcomes, Err err ) ->
|
|
Err [ err ]
|
|
|
|
( Err errors, Err err ) ->
|
|
Err (err :: errors)
|
|
|
|
|
|
{-| Create a `Decoder` from a `(Json -> Result)` function.
|
|
|
|
One place this is useful is with the [`elm-lang/http`](http://package.elm-lang.org/packages/elm-lang/http)
|
|
library, which works with `Decoder` values.
|
|
|
|
httpRequest : Http.Request User
|
|
httpRequest =
|
|
Http.get "http://example.com/user" (toDecoder decodeUser)
|
|
|
|
|
|
-- See the decodeObject documentation for an example of
|
|
-- how to implement a function like this:
|
|
|
|
decodeUser : Json -> Result String User
|
|
|
|
-}
|
|
toDecoder : (Json -> Result String a) -> Decoder a
|
|
toDecoder toResult =
|
|
let
|
|
resolve json =
|
|
case toResult (JsonValue json) of
|
|
Ok val ->
|
|
Decode.succeed val
|
|
|
|
Err err ->
|
|
Decode.fail err
|
|
in
|
|
Decode.value
|
|
|> Decode.andThen resolve
|
|
|
|
|
|
{-| Returns a string describing any errors that resulted
|
|
from decoding a [`Json`](#Json) value.
|
|
-}
|
|
errorsToString : List Outcome -> String
|
|
errorsToString errors =
|
|
-- TODO handle the case where there are no DecodingError values in the list
|
|
List.filterMap errorToMaybeString errors
|
|
|> String.join "\n\n"
|
|
|
|
|
|
|
|
-- INTERNAL HELPERS --
|
|
|
|
|
|
errorToMaybeString : Outcome -> Maybe String
|
|
errorToMaybeString error =
|
|
case error of
|
|
DecodingError str ->
|
|
Just str
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
decodeField :
|
|
Json
|
|
-> Field
|
|
-> Result (List Outcome) (List Outcome)
|
|
-> Result (List Outcome) (List Outcome)
|
|
decodeField json field result =
|
|
let
|
|
decoder =
|
|
case field of
|
|
Required path ->
|
|
Decode.at path outcomeDecoder
|
|
|
|
Optional path ->
|
|
Decode.at path maybeOutcomeDecoder
|
|
in
|
|
case ( result, decodeFromDecoder decoder json ) of
|
|
( Ok outcomes, Ok outcome ) ->
|
|
Ok (outcome :: outcomes)
|
|
|
|
( (Err _) as outcomes, Ok _ ) ->
|
|
outcomes
|
|
|
|
( Ok outcomes, Err err ) ->
|
|
Err [ DecodingError err ]
|
|
|
|
( Err errors, Err err ) ->
|
|
Err (DecodingError err :: errors)
|
|
|
|
|
|
decodeFromDecoder : Decoder a -> Json -> Result String a
|
|
decodeFromDecoder decoder json =
|
|
case json of
|
|
JsonString str ->
|
|
Decode.decodeString decoder str
|
|
|
|
JsonValue val ->
|
|
Decode.decodeValue decoder val
|
|
|
|
|
|
outcomeDecoder : Decoder Outcome
|
|
outcomeDecoder =
|
|
Decode.oneOf
|
|
[ Decode.map String Decode.string
|
|
, Decode.map Bool Decode.bool
|
|
, Decode.map numberFromTuple numberDecoder
|
|
, Decode.map List (Decode.list jsonDecoder)
|
|
, Decode.map Object jsonDecoder
|
|
]
|
|
|
|
|
|
numberFromTuple : ( Int, Float ) -> Outcome
|
|
numberFromTuple ( int, float ) =
|
|
Number int float
|
|
|
|
|
|
maybeNumberFromTuple : Maybe ( Int, Float ) -> Outcome
|
|
maybeNumberFromTuple tuple =
|
|
case tuple of
|
|
Just ( int, float ) ->
|
|
MaybeNumber (Just int) (Just float)
|
|
|
|
Nothing ->
|
|
MaybeNumber Nothing Nothing
|
|
|
|
|
|
numberDecoder : Decoder ( Int, Float )
|
|
numberDecoder =
|
|
Decode.float
|
|
|> Decode.map (\num -> ( truncate num, num ))
|
|
|
|
|
|
maybeOutcomeDecoder : Decoder Outcome
|
|
maybeOutcomeDecoder =
|
|
Decode.oneOf
|
|
[ Decode.map MaybeString (Decode.nullable Decode.string)
|
|
, Decode.map MaybeBool (Decode.nullable Decode.bool)
|
|
, Decode.map maybeNumberFromTuple (Decode.nullable numberDecoder)
|
|
, Decode.map MaybeList (Decode.nullable (Decode.list jsonDecoder))
|
|
, Decode.map MaybeObject (Decode.nullable jsonDecoder)
|
|
]
|
|
|
|
|
|
jsonDecoder : Decoder Json
|
|
jsonDecoder =
|
|
Decode.map JsonValue Decode.value
|