elm-community/json-extra/1.0.0

NOTE: this package is not compatible with Elm 0.19.1

Json.Decode.Extra

Convenience functions for working with Json

Date

date : Decoder Date

Extract a date using Date.fromString

Incremental Decoding

apply : Decoder (a -> b) -> Decoder a -> Decoder b

Can be helpful when decoding large objects incrementally.

import Date (Date)


type alias User =
  { id                : Int
  , createdAt         : Date
  , updatedAt         : Date
  , deletedAt         : Maybe Date
  , username          : Maybe String
  , email             : Maybe String
  , fullname          : Maybe String
  , avatar            : Maybe String
  , isModerator       : Bool
  , isOrganization    : Bool
  , isAdmin           : Bool
  }


metaDecoder : (Int -> Date -> Date -> Maybe Date -> b) -> Decoder b
metaDecoder f = f
  `map`      ("id"        := int)
  `apply` ("createdAt" := date)
  `apply` ("updatedAt" := date)
  `apply` ("deletedAt" := maybe date)


userDecoder : Decoder User
userDecoder = metaDecoder User
  `apply` ("username"          := maybe string)
  `apply` ("email"             := maybe string)
  `apply` ("fullname"          := maybe string)
  `apply` ("avatar"            := maybe string)
  `apply` ("isModerator"       := bool)
  `apply` ("isOrganization"    := bool)
  `apply` ("isAdmin"           := bool)

This is a shortened form of

metaDecoder : (Int -> Date -> Date -> Maybe Date -> b) -> Decoder b
metaDecoder f = f
  `map`      ("id"        := int)
  `andThen` \f -> f `map` ("createdAt" := date)
  `andThen` \f -> f `map` ("updatedAt" := date)
  `andThen` \f -> f `map` ("deletedAt" := maybe date)


userDecoder : Decoder User
userDecoder = metaDecoder User
  `andThen` \f -> f `map` ("username"          := maybe string)
  `andThen` \f -> f `map` ("email"             := maybe string)
  `andThen` \f -> f `map` ("fullname"          := maybe string)
  `andThen` \f -> f `map` ("avatar"            := maybe string)
  `andThen` \f -> f `map` ("isModerator"       := bool)
  `andThen` \f -> f `map` ("isOrganization"    := bool)
  `andThen` \f -> f `map` ("isAdmin"           := bool)
(|:) : Decoder (a -> b) -> Decoder a -> Decoder b

Infix version of apply that makes for a nice DSL when decoding objects:

locationDecoder : Decoder Location
locationDecoder =
    succeed Location
        |: ("id" := int)
        |: ("name" := string)
        |: ("address" := string)




type alias Location =
    { id : Int
    , name : String
    , address : String
    }

If you're curious, here's how this works behind the scenes.

Location is a type alias, and type aliases give you a convenience function that returns an instance of the record in question. Try this out in elm repl:

> type alias Location = { id : Int, name: String, address: String }


> Location
<function> : Int -> String -> String -> Repl.Location


> Location 1 "The White House" "1600 Pennsylvania Ave"
{ id = 1, name = "The White House", address = "1600 Pennsylvania Ave" }

In other words, if you call the Location function, passing three arguments, it will return a new Location record by filling in each of its fields. (The argument order is based on the order in which we listed the fields in the type alias; the first argument sets id, the second argument sets name, etc.)

Now try running this through elm repl:

> import Json.Decode exposing (succeed, int, string, (:=))


> succeed Location
<function>
    : Json.Decode.Decoder
        (Int -> String -> String -> Repl.Location)

So succeed Location gives us a Decoder (Int -> String -> String -> Location). That's not what we want! What we want is a Decoder Location. All we have so far is a Decoder that wraps not a Location, but rather a function that returns a Location.

What |: ("id" := int) does is to take that wrapped function and pass an argument to it.

> import Json.Decode exposing (succeed, int, string, (:=))


> ("id" := int)
<function> : Json.Decode.Decoder Int


> succeed Location |: ("id" := int)
<function>
    : Json.Decode.Decoder
        (String -> String -> Repl.Location)

Notice how the wrapped function no longer takes an Int as its first argument. That's because |: went ahead and supplied one: the Int wrapped by the decoder ("id" := int) (which returns a Decoder Int).

Compare:

-- succeed Location
Decoder (Int -> String -> String -> Location)


-- succeed Location |: ("id" := int)
Decoder (String -> String -> Location)

We still want a Decoder Location and we still don't have it yet. Our decoder still wraps a function instead of a plain Location. However, that function is now smaller by one argument!

Let's repeat this pattern to provide the first String argument next.

-- succeed Location
Decoder (Int -> String -> String -> Location)


-- succeed Location |: ("id" := int)
Decoder (String -> String -> Location)


-- succeed Location |: ("id" := int) |: ("name" := string)
Decoder (String -> Location)

Smaller and smaller! Now we're down from (Int -> String -> String -> Location) to (String -> Location). What happens if we repeat the pattern one more time?

-- succeed Location
Decoder (Int -> String -> String -> Location)


-- succeed Location |: ("id" := int)
Decoder (String -> String -> Location)


-- succeed Location |: ("id" := int) |: ("name" := string)
Decoder (String -> Location)


-- succeed Location |: ("id" := int) |: ("name" := string) |: ("address" := string)
Decoder Location

Having now supplied all three arguments to the wrapped function, it has ceased to be a function. It's now just a plain old Location, like we wanted all along.

We win!

Set

set : Decoder comparable -> Decoder (Set comparable)

Extract a set.

Dict

dict2 : Decoder comparable -> Decoder v -> Decoder (Dict comparable v)

Extract a dict using separate decoders for keys and values.

Maybe

withDefault : a -> Decoder a -> Decoder a

Try running the given decoder; if that fails, then succeed with the given fallback value.

-- If this field is missing or malformed, it will decode to [].
("optionalNames" := list string)
  |> (withDefault [])
maybeNull : Decoder a -> Decoder (Maybe a)

Extract a value that might be null. If the value is null, succeed with Nothing. If the value is present but not null, succeed with Just that value. If the value is missing, fail.

-- Yields Nothing if middleName is null, and Just middleName if it's a string.
"middleName" := maybeNull string

Recursively Defined Decoders

lazy : (() -> Decoder a) -> Decoder a

Enable decoders defined in terms of themselves by lazily creating them.

treeNode =
  object2
    instantiateTreeNode
    ("name" := string)
    ("children" := list (lazy (\_ -> treeNode)))